diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index be0ff8a..5b3ad81 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - php: [ '7.4', '8.0', '8.1', '8.2', '8.3' ] + php: [ '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] name: - mac-x64 diff --git a/CHANGELOG.md b/CHANGELOG.md index 266d509..0fdd3f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [v0.4.16](https://github.com/NoiseByNorthwest/php-spx/compare/0.4.15...0.4.16) +## [v0.4.17](https://github.com/NoiseByNorthwest/php-spx/compare/v0.4.16...v0.4.17) + +### Added +- PHP 8.4 support [#270](https://github.com/NoiseByNorthwest/php-spx/pull/270) +- Subnet mask support for IP matching [#261](https://github.com/NoiseByNorthwest/php-spx/pull/261) +- ZTS PHP beta support [#260](https://github.com/NoiseByNorthwest/php-spx/pull/260) + +### Fixed +- Keep scroll position in flatprofile view on repaint [#267](https://github.com/NoiseByNorthwest/php-spx/pull/267) +- Improved confinement check for file access from WEB UI [#255](https://github.com/NoiseByNorthwest/php-spx/pull/255) + +## [v0.4.16](https://github.com/NoiseByNorthwest/php-spx/compare/v0.4.15...v0.4.16) ### Added - Local snapshots of jQuery & jscolor in order to remove the internet access dependency of the Web UI [#217](https://github.com/NoiseByNorthwest/php-spx/issues/217) @@ -12,12 +23,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - Fixed unconfined server file access from WEB UI [#251](https://github.com/NoiseByNorthwest/php-spx/issues/251) -## [v0.4.15](https://github.com/NoiseByNorthwest/php-spx/compare/0.4.14...0.4.15) +## [v0.4.15](https://github.com/NoiseByNorthwest/php-spx/compare/v0.4.14...v0.4.15) ### Added - Added Debian builds to github workflow [#222](https://github.com/NoiseByNorthwest/php-spx/pull/222) -- Added PHP8.3 support [#230](https://github.com/NoiseByNorthwest/php-spx/pull/230) -- Added PHP5.4 support [#227](https://github.com/NoiseByNorthwest/php-spx/pull/227) +- Added PHP 8.3 support [#230](https://github.com/NoiseByNorthwest/php-spx/pull/230) +- Added PHP 5.4 support [#227](https://github.com/NoiseByNorthwest/php-spx/pull/227) ### Fixed - Fixed ESM module imports in Web UI [#233](https://github.com/NoiseByNorthwest/php-spx/issues/233) @@ -25,7 +36,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed non-portable path expectation in tests [#221](https://github.com/NoiseByNorthwest/php-spx/pull/221) -## [v0.4.14](https://github.com/NoiseByNorthwest/php-spx/compare/0.4.13...0.4.14) +## [v0.4.14](https://github.com/NoiseByNorthwest/php-spx/compare/v0.4.13...v0.4.14) ### Added - Added simple search feature @@ -36,7 +47,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed buffer overflow in str_builder -## [v0.4.13](https://github.com/NoiseByNorthwest/php-spx/compare/0.4.12...0.4.13) +## [v0.4.13](https://github.com/NoiseByNorthwest/php-spx/compare/v0.4.12...v0.4.13) ### Added - Github Actions workflow ([#180](https://github.com/NoiseByNorthwest/php-spx/issues/180)) @@ -50,7 +61,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Travis CI [#195](https://github.com/NoiseByNorthwest/php-spx/issues/195) -## [v0.4.0](https://github.com/NoiseByNorthwest/php-spx/compare/0.3.0...0.4.0) +## [v0.4.0](https://github.com/NoiseByNorthwest/php-spx/compare/v0.3.0...v0.4.0) ### Added - Analysis screen: fix the NaNs in time grid when time goes above 1000s (fixes [#65](https://github.com/NoiseByNorthwest/php-spx/issues/65)) @@ -66,7 +77,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fix build on macOS 10.11- (fixes [#78](https://github.com/NoiseByNorthwest/php-spx/pull/78)) -## [v0.3.0](https://github.com/NoiseByNorthwest/php-spx/compare/0.2.0...0.3.0) +## [v0.3.0](https://github.com/NoiseByNorthwest/php-spx/compare/v0.2.0...v0.3.0) ### Added - Improve documentation around web UI requirements (fixes [#54](https://github.com/NoiseByNorthwest/php-spx/issues/54)) @@ -93,7 +104,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Web UI / analysis screen: improved layout -## [v0.2.0](https://github.com/NoiseByNorthwest/php-spx/compare/0.1.2...0.2.0) +## [v0.2.0](https://github.com/NoiseByNorthwest/php-spx/compare/v0.1.2...v0.2.0) ### Added - Add web UI ([#14](https://github.com/NoiseByNorthwest/php-spx/pull/14)) @@ -106,7 +117,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Remove Callgrind report type ([#11](https://github.com/NoiseByNorthwest/php-spx/issues/11)) -## [v0.1.2](https://github.com/NoiseByNorthwest/php-spx/compare/0.1.2...0.1.2) +## [v0.1.2](https://github.com/NoiseByNorthwest/php-spx/compare/v0.1.2...v0.1.2) ### Added - Add macOS support ([orls](https://github.com/orls) in [#13](https://github.com/NoiseByNorthwest/php-spx/pull/13)) diff --git a/README.md b/README.md index cc5ce6d..0971c53 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ See the [LICENSE][:link-license:] file for more information. [:badge-ci:]: https://github.com/NoiseByNorthwest/php-spx/actions/workflows/main.yml/badge.svg [:link-ci:]: https://github.com/NoiseByNorthwest/php-spx/actions/workflows/main.yml -[:badge-php-versions:]: https://img.shields.io/badge/php-5.4--8.3-blue.svg +[:badge-php-versions:]: https://img.shields.io/badge/php-5.4--8.4-blue.svg [:badge-supported-platforms:]: https://img.shields.io/badge/platform-GNU/Linux%20|%20macOS%20|%20FreeBSD%20-yellow [:badge-supported-arch:]: https://img.shields.io/badge/architecture-x86--64%20|%20ARM64%20-silver diff --git a/assets/web-ui/js/widget.js b/assets/web-ui/js/widget.js index 9bcd906..d61d062 100644 --- a/assets/web-ui/js/widget.js +++ b/assets/web-ui/js/widget.js @@ -573,21 +573,17 @@ class Widget { this.repaintTimeout = setTimeout( () => { + let initialScrollPos = 0; this.repaintTimeout = null; const id = this.container.attr('id'); - // Remember the scroll position - let scroll = 0; if (id === 'flatprofile') { - scroll = $(`#flatprofile div`).scrollTop(); + initialScrollPos = document.querySelector('#flatprofile > div').scrollTop; } this.clear(); this.render(); - // Restore the scroll position if (id === 'flatprofile') { - $(`#flatprofile div`).scrollTop(scroll); - } else { - $(`#${id}`).scrollTop(scroll); + document.querySelector('#flatprofile > div').scrollTop = initialScrollPos; } }, 0 diff --git a/config.m4 b/config.m4 index 0c11dd0..1415251 100644 --- a/config.m4 +++ b/config.m4 @@ -23,6 +23,12 @@ if test "$PHP_SPX" = "yes"; then CFLAGS="$CFLAGS -DCONTINUOUS_INTEGRATION" fi + if test "$(uname -s 2>/dev/null)" = "Darwin" + then + # see discussion here https://github.com/NoiseByNorthwest/php-spx/pull/270 + CFLAGS="$CFLAGS -Wno-typedef-redefinition" + fi + if test "$PHP_SPX_DEV" = "yes" then CFLAGS="$CFLAGS -g" diff --git a/src/php_spx.c b/src/php_spx.c index 6d5150f..92a7574 100644 --- a/src/php_spx.c +++ b/src/php_spx.c @@ -552,7 +552,7 @@ static int check_access(void) int found = 0; SPX_UTILS_TOKENIZE_STRING(SPX_G(http_trusted_proxies), ',', trusted_proxy_ip_str, 64, { - if (0 == strcmp(proxy_ip_str, trusted_proxy_ip_str)) { + if (spx_utils_ip_match(proxy_ip_str, trusted_proxy_ip_str)) { found = 1; } }); @@ -586,18 +586,13 @@ static int check_access(void) } SPX_UTILS_TOKENIZE_STRING(authorized_ips_str, ',', authorized_ip_str, 64, { - if (0 == strcmp(ip_str, authorized_ip_str)) { + if (spx_utils_ip_match(ip_str, authorized_ip_str)) { /* ip authorized (OK, as well as all previous checks) -> granted */ return 1; } }); - if (0 == strcmp(authorized_ips_str, "*")) { - /* all ips authorized */ - return 1; - } - spx_php_log_notice( "access not granted: \"%s\" IP is not in white list (\"%s\")", ip_str, diff --git a/src/php_spx.h b/src/php_spx.h index ad95372..0a71435 100644 --- a/src/php_spx.h +++ b/src/php_spx.h @@ -31,8 +31,8 @@ # error "Only x86-64 and ARM64 architectures are supported" #endif -#if ZEND_MODULE_API_NO < 20100525 || ZEND_MODULE_API_NO > 20230831 // 8.3-RC5 -# error "Only the following PHP versions are supported: 5.4 to 8.3" +#if ZEND_MODULE_API_NO < 20100525 || ZEND_MODULE_API_NO > 20240924 +# error "Only the following PHP versions are supported: 5.4 to 8.4" #endif #if defined(ZTS) && !defined(CONTINUOUS_INTEGRATION) @@ -40,6 +40,6 @@ #endif #define PHP_SPX_EXTNAME "SPX" -#define PHP_SPX_VERSION "0.4.15" +#define PHP_SPX_VERSION "0.4.17" extern zend_module_entry spx_module_entry; diff --git a/src/spx_php.c b/src/spx_php.c index f68ca64..12051cf 100644 --- a/src/spx_php.c +++ b/src/spx_php.c @@ -74,13 +74,6 @@ typedef void (*execute_internal_func_t) ( ); static struct { -#if ZEND_MODULE_API_NO >= 20151012 - void * (*malloc) (size_t size); - void (*free) (void * ptr); - void * (*realloc) (void * ptr, size_t size); - size_t (*block_size) (void * ptr); -#endif - #if ZEND_MODULE_API_NO < 20121212 void (*execute) (zend_op_array * op_array TSRMLS_DC); #else @@ -124,9 +117,6 @@ static struct { #endif ); } ze_hooked_func = { -#if ZEND_MODULE_API_NO >= 20151012 - NULL, NULL, NULL, NULL, -#endif NULL, NULL, NULL, NULL, NULL, #if ZEND_MODULE_API_NO >= 20151012 @@ -135,6 +125,17 @@ static struct { NULL }; +#if ZEND_MODULE_API_NO >= 20151012 +static SPX_THREAD_TLS struct { + void * (*malloc) (size_t size); + void (*free) (void * ptr); + void * (*realloc) (void * ptr, size_t size); + size_t (*block_size) (void * ptr); +} ze_tls_hooked_func = { + NULL, NULL, NULL, NULL +}; +#endif + static SPX_THREAD_TLS struct { struct { struct { @@ -664,24 +665,28 @@ void spx_php_execution_init(void) #if ZEND_MODULE_API_NO >= 20151012 zend_mm_heap * ze_mm_heap = zend_mm_get_heap(); + /* + * FIXME document why we need ze_mm_custom_block_size instead of ze_mm_block_size + * when there is no previous MM custom handler. + */ + ze_tls_hooked_func.block_size = ze_mm_custom_block_size; + zend_mm_get_custom_handlers( ze_mm_heap, - &ze_hooked_func.malloc, - &ze_hooked_func.free, - &ze_hooked_func.realloc + &ze_tls_hooked_func.malloc, + &ze_tls_hooked_func.free, + &ze_tls_hooked_func.realloc ); - ze_hooked_func.block_size = ze_mm_custom_block_size; - if ( - !ze_hooked_func.malloc - || !ze_hooked_func.free - || !ze_hooked_func.realloc + !ze_tls_hooked_func.malloc + || !ze_tls_hooked_func.free + || !ze_tls_hooked_func.realloc ) { - ze_hooked_func.malloc = ze_mm_malloc; - ze_hooked_func.free = ze_mm_free; - ze_hooked_func.realloc = ze_mm_realloc; - ze_hooked_func.block_size = ze_mm_block_size; + ze_tls_hooked_func.malloc = ze_mm_malloc; + ze_tls_hooked_func.free = ze_mm_free; + ze_tls_hooked_func.realloc = ze_mm_realloc; + ze_tls_hooked_func.block_size = ze_mm_block_size; } zend_mm_set_custom_handlers( @@ -697,24 +702,24 @@ void spx_php_execution_shutdown(void) { #if ZEND_MODULE_API_NO >= 20151012 if ( - ze_hooked_func.malloc - && ze_hooked_func.free - && ze_hooked_func.realloc + ze_tls_hooked_func.malloc + && ze_tls_hooked_func.free + && ze_tls_hooked_func.realloc ) { zend_mm_heap * ze_mm_heap = zend_mm_get_heap(); if ( /* - * ze_hooked_func.malloc was defaulted to ze_mm_malloc only if there were no + * ze_tls_hooked_func.malloc was defaulted to ze_mm_malloc only if there were no * previous custom handlers. */ - ze_hooked_func.malloc != ze_mm_malloc + ze_tls_hooked_func.malloc != ze_mm_malloc ) { zend_mm_set_custom_handlers( ze_mm_heap, - ze_hooked_func.malloc, - ze_hooked_func.free, - ze_hooked_func.realloc + ze_tls_hooked_func.malloc, + ze_tls_hooked_func.free, + ze_tls_hooked_func.realloc ); } else { /* @@ -733,9 +738,10 @@ void spx_php_execution_shutdown(void) } } - ze_hooked_func.malloc = NULL; - ze_hooked_func.free = NULL; - ze_hooked_func.realloc = NULL; + ze_tls_hooked_func.malloc = NULL; + ze_tls_hooked_func.free = NULL; + ze_tls_hooked_func.realloc = NULL; + ze_tls_hooked_func.block_size = NULL; } #endif @@ -1013,11 +1019,11 @@ static void * ze_mm_realloc(void * ptr, size_t size) static void * tls_hook_malloc(size_t size) { - void * ptr = ze_hooked_func.malloc(size); + void * ptr = ze_tls_hooked_func.malloc(size); if (ptr) { context.alloc_count++; - context.alloc_bytes += ze_hooked_func.block_size(ptr); + context.alloc_bytes += ze_tls_hooked_func.block_size(ptr); } return ptr; @@ -1027,17 +1033,17 @@ static void tls_hook_free(void * ptr) { if (ptr) { context.free_count++; - context.free_bytes += ze_hooked_func.block_size(ptr); + context.free_bytes += ze_tls_hooked_func.block_size(ptr); } - ze_hooked_func.free(ptr); + ze_tls_hooked_func.free(ptr); } static void * tls_hook_realloc(void * ptr, size_t size) { - const size_t old_size = ptr ? ze_hooked_func.block_size(ptr) : 0; - void * new = ze_hooked_func.realloc(ptr, size); - const size_t new_size = new ? ze_hooked_func.block_size(new) : 0; + const size_t old_size = ptr ? ze_tls_hooked_func.block_size(ptr) : 0; + void * new = ze_tls_hooked_func.realloc(ptr, size); + const size_t new_size = new ? ze_tls_hooked_func.block_size(new) : 0; if (ptr && new) { if (ptr != new) { diff --git a/src/spx_utils.c b/src/spx_utils.c index e846e2c..ba4b284 100644 --- a/src/spx_utils.c +++ b/src/spx_utils.c @@ -19,7 +19,72 @@ #include #include #include + +#ifdef ZTS +# include +#endif + +#include + #include "spx_utils.h" +#include "spx_php.h" + +int spx_utils_ip_match(const char * ip_address_str, const char * target) +{ + if ( + strcmp(target, "*") == 0 || + strcmp(target, ip_address_str) == 0 + ) { + return 1; + } + + // subnet handling + + const char * slash_ptr = strchr(target, '/'); + if (slash_ptr == NULL) { + return 0; + } + + const size_t slash_pos = slash_ptr - target; + if (! (7 <= slash_pos && slash_pos <= 15)) { + return 0; + } + + const size_t target_suffix_len = strlen(slash_ptr); + if (! (2 <= target_suffix_len && target_suffix_len <= 3)) { + return 0; + } + + char target_ip_address_str[32]; + strncpy(target_ip_address_str, target, sizeof target_ip_address_str); + target_ip_address_str[slash_pos] = 0; + + const in_addr_t target_ip_address = inet_addr(target_ip_address_str); + if (target_ip_address == INADDR_NONE) { + return 0; + } + + char target_mask_str[32]; + snprintf(target_mask_str, sizeof target_mask_str, "%s", slash_ptr + 1); + const long target_mask_bits = strtol(target_mask_str, NULL, 10); + + if (! (1 <= target_mask_bits && target_mask_bits <= 31)) { + return 0; + } + + const in_addr_t target_mask = (~0) << (32 - target_mask_bits); + + const in_addr_t ip_address = inet_addr(ip_address_str); + if (ip_address == INADDR_NONE) { + return 0; + } + + if ((ntohl(ip_address) & target_mask) == (ntohl(target_ip_address) & target_mask)) { + return 1; + } + + return 0; +} char * spx_utils_json_escape(char * dst, const char * src, size_t limit) { diff --git a/src/spx_utils.h b/src/spx_utils.h index 370818a..b7b8252 100644 --- a/src/spx_utils.h +++ b/src/spx_utils.h @@ -49,6 +49,16 @@ do { \ } \ } while (0) +int spx_utils_ip_match(const char * ip_address, const char * target); + +char * spx_utils_resolve_confined_file_absolute_path( + const char * root_dir, + const char * relative_path, + const char * suffix, + char * dst, + size_t size +); + char * spx_utils_json_escape(char * dst, const char * src, size_t limit); int spx_utils_str_starts_with(const char * str, const char * prefix); int spx_utils_str_ends_with(const char * str, const char * suffix); diff --git a/tests/spx_auth_ip_subnet_ko_1.phpt b/tests/spx_auth_ip_subnet_ko_1.phpt new file mode 100644 index 0000000..ebd68aa --- /dev/null +++ b/tests/spx_auth_ip_subnet_ko_1.phpt @@ -0,0 +1,23 @@ +--TEST-- +Authentication: KO (invalid IP address) +--CGI-- +--INI-- +spx.http_enabled=1 +spx.http_key="dev" +spx.http_ip_whitelist="10.0.0.0/24" +spx.http_ui_assets_dir="{PWD}/../assets/web-ui" +log_errors=on +--ENV-- +return << +--EXPECT-- +Notice: SPX: access not granted: "10.0.1.1" IP is not in white list ("10.0.0.0/24") in Unknown on line 0 +Normal output \ No newline at end of file diff --git a/tests/spx_auth_ip_subnet_ko_2.phpt b/tests/spx_auth_ip_subnet_ko_2.phpt new file mode 100644 index 0000000..c131634 --- /dev/null +++ b/tests/spx_auth_ip_subnet_ko_2.phpt @@ -0,0 +1,23 @@ +--TEST-- +Authentication: KO (invalid IP address) +--CGI-- +--INI-- +spx.http_enabled=1 +spx.http_key="dev" +spx.http_ip_whitelist="10.0.0.0/0" +spx.http_ui_assets_dir="{PWD}/../assets/web-ui" +log_errors=on +--ENV-- +return << +--EXPECT-- +Notice: SPX: access not granted: "10.0.0.1" IP is not in white list ("10.0.0.0/0") in Unknown on line 0 +Normal output \ No newline at end of file diff --git a/tests/spx_auth_ip_subnet_ko_3.phpt b/tests/spx_auth_ip_subnet_ko_3.phpt new file mode 100644 index 0000000..65a2f10 --- /dev/null +++ b/tests/spx_auth_ip_subnet_ko_3.phpt @@ -0,0 +1,23 @@ +--TEST-- +Authentication: KO (invalid IP address) +--CGI-- +--INI-- +spx.http_enabled=1 +spx.http_key="dev" +spx.http_ip_whitelist="10.0.0.0/32" +spx.http_ui_assets_dir="{PWD}/../assets/web-ui" +log_errors=on +--ENV-- +return << +--EXPECT-- +Notice: SPX: access not granted: "10.0.0.1" IP is not in white list ("10.0.0.0/32") in Unknown on line 0 +Normal output \ No newline at end of file diff --git a/tests/spx_auth_ip_subnet_ok.phpt b/tests/spx_auth_ip_subnet_ok.phpt new file mode 100644 index 0000000..7bbf297 --- /dev/null +++ b/tests/spx_auth_ip_subnet_ok.phpt @@ -0,0 +1,45 @@ +--TEST-- +Authentication: OK (valid IP address) +--CGI-- +--INI-- +spx.http_enabled=1 +spx.http_key="dev" +spx.http_ip_whitelist="10.0.0.0/24" +spx.http_ui_assets_dir="{PWD}/../assets/web-ui" +log_errors=on +--ENV-- +return << +--EXPECT-- +{"results": [ +{"key": "wt","short_name": "Wall time","name": "Wall time","type": "time","releasable": 0} +,{"key": "ct","short_name": "CPU time","name": "CPU time","type": "time","releasable": 0} +,{"key": "it","short_name": "Idle time","name": "Idle time","type": "time","releasable": 0} +,{"key": "zm","short_name": "ZE memory usage","name": "Zend Engine memory usage","type": "memory","releasable": 1} +,{"key": "zmac","short_name": "ZE alloc count","name": "Zend Engine allocation count","type": "quantity","releasable": 0} +,{"key": "zmab","short_name": "ZE alloc bytes","name": "Zend Engine allocated bytes","type": "memory","releasable": 0} +,{"key": "zmfc","short_name": "ZE free count","name": "Zend Engine free count","type": "quantity","releasable": 0} +,{"key": "zmfb","short_name": "ZE free bytes","name": "Zend Engine freed bytes","type": "memory","releasable": 0} +,{"key": "zgr","short_name": "ZE GC runs","name": "Zend Engine GC run count","type": "quantity","releasable": 0} +,{"key": "zgb","short_name": "ZE GC root buffer","name": "Zend Engine GC root buffer length","type": "quantity","releasable": 1} +,{"key": "zgc","short_name": "ZE GC collected","name": "Zend Engine GC collected cycle count","type": "quantity","releasable": 0} +,{"key": "zif","short_name": "ZE file count","name": "Zend Engine included file count","type": "quantity","releasable": 0} +,{"key": "zil","short_name": "ZE line count","name": "Zend Engine included line count","type": "quantity","releasable": 0} +,{"key": "zuc","short_name": "ZE class count","name": "Zend Engine user class count","type": "quantity","releasable": 0} +,{"key": "zuf","short_name": "ZE func. count","name": "Zend Engine user function count","type": "quantity","releasable": 0} +,{"key": "zuo","short_name": "ZE opcodes count","name": "Zend Engine user opcode count","type": "quantity","releasable": 0} +,{"key": "zo","short_name": "ZE object count","name": "Zend Engine object count","type": "quantity","releasable": 1} +,{"key": "ze","short_name": "ZE error count","name": "Zend Engine error count","type": "quantity","releasable": 0} +,{"key": "mor","short_name": "Own RSS","name": "Process's own RSS","type": "memory","releasable": 1} +,{"key": "io","short_name": "I/O Bytes","name": "I/O Bytes (reads + writes)","type": "memory","releasable": 0} +,{"key": "ior","short_name": "I/O Read Bytes","name": "I/O Read Bytes","type": "memory","releasable": 0} +,{"key": "iow","short_name": "I/O Written Bytes","name": "I/O Written Bytes","type": "memory","releasable": 0} +]} \ No newline at end of file