diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000..60e996b
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,15 @@
+{
+ "name": "colopl_timeshifter",
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "ms-vscode.cpptools",
+ "ms-vscode.cpptools-extension-pack",
+ "maelvalais.autoconf"
+ ]
+ }
+ },
+ "dockerComposeFile": "../compose.yaml",
+ "service": "dev",
+ "workspaceFolder": "/usr/src/php/ext/extension"
+}
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..6b8710a
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1 @@
+.git
diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml
new file mode 100644
index 0000000..c905fdd
--- /dev/null
+++ b/.github/dependabot.yaml
@@ -0,0 +1,17 @@
+version: 2
+updates:
+ - package-ecosystem: "composer"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ day: "saturday"
+ - package-ecosystem: "docker"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ day: "saturday"
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ day: "saturday"
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
new file mode 100644
index 0000000..369759a
--- /dev/null
+++ b/.github/workflows/build.yaml
@@ -0,0 +1,42 @@
+name: Build
+on:
+ push:
+ tags:
+ - '[0-9]+.[0-9]+.[0-9]+'
+jobs:
+ ubuntu_2204_php81_origin_deb:
+ runs-on: ubuntu-22.04
+ timeout-minutes: 60
+ strategy:
+ matrix:
+ arch: ["arm64v8", "amd64"]
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ submodules: true
+ - name: Setup QEMU
+ uses: docker/setup-qemu-action@v3
+ with:
+ platforms: arm64
+ - name: Setup Buildx
+ uses: docker/setup-buildx-action@v3
+ - name: Build Container
+ uses: docker/build-push-action@v6
+ with:
+ build-args: ARCH=${{ matrix.arch }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+ context: .
+ file: ./build/ubuntu2204/Dockerfile
+ load: true
+ tags: "pskel-build-ubuntu2204-${{ matrix.arch }}"
+ - name: Build Extension with Container
+ run: |
+ mkdir "artifacts"
+ docker run --env VERSION="${{ github.ref_name }}" --rm -v"$(pwd)/artifacts:/tmp/artifacts" -i "pskel-build-ubuntu2204-${{ matrix.arch }}"
+ - name: Upload deb Packages
+ uses: actions/upload-artifact@v4
+ with:
+ name: ubuntu_2204_debs-${{ matrix.arch }}
+ path: artifacts/
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 0000000..810182a
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,49 @@
+name: CI
+on: [push, pull_request]
+jobs:
+ CI:
+ runs-on: ubuntu-22.04
+ timeout-minutes: 60
+ strategy:
+ matrix:
+ arch: ["amd64", "arm64v8", "s390x"]
+ version: ["8.1", "8.2", "8.3"]
+ type: ["cli", "zts"]
+ distro: ["bookworm", "alpine"]
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ submodules: true
+ - name: Setup QEMU
+ uses: docker/setup-qemu-action@v3
+ with:
+ platforms: "arm64,s390x"
+ - name: Setup buildx
+ uses: docker/setup-buildx-action@v3
+ - name: Build container
+ run: |
+ docker compose build --pull --no-cache --build-arg IMAGE=${{ matrix.arch }}/php --build-arg TAG=${{ matrix.version }}-${{ matrix.type }}-${{ matrix.distro }} --build-arg PSKEL_SKIP_DEBUG=${{ matrix.arch != 'amd64' && '1' || '' }}
+ - name: Run tests
+ run: |
+ docker compose run --rm --entrypoint=/usr/bin/ci --env TEST_EXTENSION=1 dev
+ - name: Test extension with PHP Debug Build
+ if: matrix.arch == 'amd64'
+ run: |
+ docker compose run --rm --entrypoint=/usr/bin/ci --env TEST_EXTENSION_DEBUG=1 dev
+ - name: Test extension with Valgrind
+ if: matrix.arch == 'amd64'
+ run: |
+ docker compose run --rm --entrypoint=/usr/bin/ci --env TEST_EXTENSION_VALGRIND=1 dev
+ - name: Test extension with LLVM Sanitizer (MemorySanitizer)
+ if: matrix.arch == 'amd64' && matrix.distro != 'alpine'
+ run: |
+ docker compose run --rm --entrypoint=/usr/bin/ci --env TEST_EXTENSION_MSAN=1 dev
+ - name: Test extension with LLVM Sanitizer (AddressSanitizer)
+ if: matrix.arch == 'amd64' && matrix.distro != 'alpine'
+ run: |
+ docker compose run --rm --entrypoint=/usr/bin/ci --env TEST_EXTENSION_ASAN=1 dev
+ - name: Test extension with LLVM Sanitizer (UndefinedBehaviorSanitizer)
+ if: matrix.arch == 'amd64' && matrix.distro != 'alpine'
+ run: |
+ docker compose run --rm --entrypoint=/usr/bin/ci --env TEST_EXTENSION_UBSAN=1 dev
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b3c4903
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+/.idea/
+/vendor/
+composer.lock
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..22c4d49
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,6 @@
+[submodule "ext/third_party/timelib"]
+ path = ext/third_party/timelib
+ url = https://github.com/derickr/timelib.git
+[submodule "derickr/timelib"]
+ path = ext/third_party/timelib
+ url = https://github.com/derickr/timelib.git
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..a1f3757
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,129 @@
+ARG IMAGE=php
+ARG TAG=8.3-cli
+
+FROM ${IMAGE}:${TAG}
+
+ARG PSKEL_SKIP_DEBUG=""
+ARG PSKEL_EXTRA_CONFIGURE_OPTIONS=""
+
+ENV USE_ZEND_ALLOC=0
+ENV ZEND_DONT_UNLOAD_MODULES=1
+ENV PSKEL_SKIP_DEBUG=${PSKEL_SKIP_DEBUG}
+ENV PSKEL_EXTRA_CONFIGURE_OPTIONS=${PSKEL_EXTRA_CONFIGURE_OPTIONS}
+
+COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
+
+RUN if test -f "/etc/debian_version"; then \
+ apt-get update && \
+ DEBIAN_FRONTEND="noninteractive" apt-get install -y \
+ "build-essential" "bison" "valgrind" "llvm" "clang" "zlib1g-dev" "libsqlite3-dev" "git" "unzip" && \
+ if test "${PSKEL_SKIP_DEBUG}" = ""; then \
+ docker-php-source extract && \
+ cd "/usr/src/php" && \
+ CFLAGS="-fpic -fpie -DZEND_TRACK_ARENA_ALLOC" LDFLAGS="-pie" ./configure --disable-all \
+ --includedir="/usr/local/include/gcc-valgrind-php" --program-prefix="gcc-valgrind-" \
+ --disable-cgi --disable-fpm --enable-cli \
+ --enable-mysqlnd --enable-pdo --with-pdo-mysql --with-pdo-sqlite \
+ --enable-debug --without-pcre-jit "$(php -r "echo PHP_ZTS === 1 ? '--enable-zts' : '';")" \
+ --with-valgrind \
+ ${PSKEL_EXTRA_CONFIGURE_OPTIONS} \
+ --enable-option-checking=fatal && \
+ make -j$(nproc) && \
+ make install && \
+ cd - && \
+ docker-php-source delete && \
+ docker-php-source extract && \
+ cd "/usr/src/php" && \
+ CC=clang CXX=clang++ CFLAGS="-fsanitize=memory -fno-sanitize-recover -DZEND_TRACK_ARENA_ALLOC" LDFLAGS="-fsanitize=memory" ./configure \
+ --includedir="/usr/local/include/clang-msan-php" --program-prefix="clang-msan-" \
+ --disable-cgi --disable-all --disable-fpm --enable-cli \
+ --enable-mysqlnd --enable-pdo --with-pdo-mysql --with-pdo-sqlite \
+ --enable-debug --without-pcre-jit "$(php -r "echo PHP_ZTS === 1 ? '--enable-zts' : '';")" \
+ --enable-memory-sanitizer \
+ ${PSKEL_EXTRA_CONFIGURE_OPTIONS} \
+ --enable-option-checking=fatal && \
+ make -j$(nproc) && \
+ make install && \
+ cd - && \
+ docker-php-source delete && \
+ docker-php-source extract && \
+ cd "/usr/src/php" && \
+ CC=clang CXX=clang++ CFLAGS="-fsanitize=address -fno-sanitize-recover -DZEND_TRACK_ARENA_ALLOC" LDFLAGS="-fsanitize=address" ./configure \
+ --includedir="/usr/local/include/clang-asan-php" --program-prefix="clang-asan-" \
+ --disable-cgi --disable-all --disable-fpm --enable-cli \
+ --enable-mysqlnd --enable-pdo --with-pdo-mysql --with-pdo-sqlite \
+ --enable-debug --without-pcre-jit "$(php -r "echo PHP_ZTS === 1 ? '--enable-zts' : '';")" \
+ --enable-address-sanitizer \
+ ${PSKEL_EXTRA_CONFIGURE_OPTIONS} \
+ --enable-option-checking=fatal && \
+ make -j$(nproc) && \
+ make install && \
+ cd - && \
+ docker-php-source delete && \
+ docker-php-source extract && \
+ cd "/usr/src/php" && \
+ CC=clang CXX=clang++ CFLAGS="-fsanitize=undefined -fno-sanitize-recover -DZEND_TRACK_ARENA_ALLOC" LDFLAGS="-fsanitize=undefined" ./configure \
+ --includedir="/usr/local/include/clang-ubsan-php" --program-prefix="clang-ubsan-" \
+ --disable-cgi --disable-all --disable-fpm --enable-cli \
+ --enable-mysqlnd --enable-pdo --with-pdo-mysql --with-pdo-sqlite \
+ --enable-debug --without-pcre-jit "$(php -r "echo PHP_ZTS === 1 ? '--enable-zts' : '';")" \
+ --enable-undefined-sanitizer \
+ ${PSKEL_EXTRA_CONFIGURE_OPTIONS} \
+ --enable-option-checking=fatal && \
+ make -j$(nproc) && \
+ make install && \
+ cd - && \
+ docker-php-source delete && \
+ docker-php-source extract && \
+ cd "/usr/src/php" && \
+ CFLAGS="-fpic -fpie -DZEND_TRACK_ARENA_ALLOC" LDFLAGS="-pie" ./configure --disable-all \
+ --includedir="/usr/local/include/debug-php" --program-prefix="debug-" \
+ --disable-cgi --disable-fpm --enable-cli \
+ --enable-mysqlnd --enable-pdo --with-pdo-mysql --with-pdo-sqlite \
+ --enable-debug "$(php -r "echo PHP_ZTS === 1 ? '--enable-zts' : '';")" \
+ ${PSKEL_EXTRA_CONFIGURE_OPTIONS} \
+ --enable-option-checking=fatal && \
+ make -j$(nproc) && \
+ make install && \
+ cd - && \
+ docker-php-source delete; \
+ fi; \
+ elif test -f "/etc/alpine-release"; then \
+ apk add --no-cache ${PHPIZE_DEPS} "bison" "valgrind" "valgrind-dev" "zlib-dev" "sqlite-dev" "git" "unzip" && \
+ if test "${PSKEL_SKIP_DEBUG}" = ""; then \
+ docker-php-source extract && \
+ cd "/usr/src/php" && \
+ CFLAGS="-fpic -fpie -DZEND_TRACK_ARENA_ALLOC" LDFLAGS="-pie" ./configure --disable-all \
+ --includedir="/usr/local/include/gcc-valgrind-php" --program-prefix="gcc-valgrind-" \
+ --disable-cgi --disable-fpm --enable-cli \
+ --enable-mysqlnd --enable-pdo --with-pdo-mysql --with-pdo-sqlite \
+ --enable-debug --without-pcre-jit "$(php -r "echo PHP_ZTS === 1 ? '--enable-zts' : '';")" \
+ --with-valgrind \
+ ${PSKEL_EXTRA_CONFIGURE_OPTIONS} \
+ --enable-option-checking=fatal && \
+ make -j$(nproc) && \
+ make install && \
+ cd - && \
+ docker-php-source delete && \
+ docker-php-source extract && \
+ cd "/usr/src/php" && \
+ CFLAGS="-fpic -fpie -DZEND_TRACK_ARENA_ALLOC" LDFLAGS="-pie" ./configure --disable-all \
+ --includedir="/usr/local/include/debug-php" --program-prefix="debug-" \
+ --disable-cgi --disable-fpm --enable-cli \
+ --enable-mysqlnd --enable-pdo --with-pdo-mysql --with-pdo-sqlite \
+ --enable-debug "$(php -r "echo PHP_ZTS === 1 ? '--enable-zts' : '';")" \
+ ${PSKEL_EXTRA_CONFIGURE_OPTIONS} \
+ --enable-option-checking=fatal && \
+ make -j$(nproc) && \
+ make install && \
+ cd - && \
+ docker-php-source delete; \
+ fi; \
+ fi && \
+ docker-php-source extract
+
+WORKDIR "/usr/src/php"
+
+COPY ./ext /ext
+
+COPY ./ci.sh /usr/bin/ci
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..4076fe9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,68 @@
+--------------------------------------------------------------------
+ The PHP License, version 3.01
+Copyright (c) 1999 - 2019 The PHP Group. All rights reserved.
+--------------------------------------------------------------------
+
+Redistribution and use in source and binary forms, with or without
+modification, is permitted provided that the following conditions
+are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ 3. The name "PHP" must not be used to endorse or promote products
+ derived from this software without prior written permission. For
+ written permission, please contact group@php.net.
+
+ 4. Products derived from this software may not be called "PHP", nor
+ may "PHP" appear in their name, without prior written permission
+ from group@php.net. You may indicate that your software works in
+ conjunction with PHP by saying "Foo for PHP" instead of calling
+ it "PHP Foo" or "phpfoo"
+
+ 5. The PHP Group may publish revised and/or new versions of the
+ license from time to time. Each version will be given a
+ distinguishing version number.
+ Once covered code has been published under a particular version
+ of the license, you may always continue to use it under the terms
+ of that version. You may also choose to use such covered code
+ under the terms of any subsequent version of the license
+ published by the PHP Group. No one other than the PHP Group has
+ the right to modify the terms applicable to covered code created
+ under this License.
+
+ 6. Redistributions of any form whatsoever must retain the following
+ acknowledgment:
+ "This product includes PHP software, freely available from
+ ".
+
+THIS SOFTWARE IS PROVIDED BY THE PHP DEVELOPMENT TEAM ``AS IS'' AND
+ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE PHP
+DEVELOPMENT TEAM OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+OF THE POSSIBILITY OF SUCH DAMAGE.
+
+--------------------------------------------------------------------
+
+This software consists of voluntary contributions made by many
+individuals on behalf of the PHP Group.
+
+The PHP Group can be contacted via Email at group@php.net.
+
+For more information on the PHP Group and the PHP project,
+please see .
+
+PHP includes the Zend Engine, freely available at
+.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0a33675
--- /dev/null
+++ b/README.md
@@ -0,0 +1,97 @@
+# colopl_timeshifter
+
+This extension changes the current time in PHP to a specified modified value.
+
+> [!WARNING]
+> **DO NOT USE THIS EXTENSION IN ANY PRODUCTION ENVIRONMENT!!!**
+
+At present, this extension is effective for the following functions:
+
+- Any built-in PHP processing that handles the current time (`ext-date`)
+- `NOW()` and many statements in MySQL or compatible DBMS via PDO
+- Server environment variables for request time (e.g. `S_SERVER['REQUEST_TIME']`)
+
+## Setup
+
+```bash
+$ git clone --recursive "https://github.com/colopl/php-colopl_timesifter.git" "colopl_timeshifter"
+$ cd "colopl_timeshifter/ext"
+$ phpize
+$ ./configure --with-php-config="$(which php-config)"
+$ make -j$(nproc)
+$ TEST_PHP_ARGS="-q --show-diff" make test
+$ sudo make install
+```
+
+And enable extension.
+
+```
+$ sudo echo "extension=colopl_timesfhiter" > "$(php-config --ini-dir)/99-colopl_timeshifter.ini"
+$ php -m | grep colopl_timeshifter
+colopl_timeshifter
+```
+
+### PHP Library (recommended)
+
+```bash
+$ composer require --dev "colopl/colopl_timeshifter"
+```
+
+And use `Colopl\ColoplTimeShifter\Manager` class.
+
+## INI directives
+
+#### `colopl_timeshifter.is_hook_pdo_mysql`
+
+Type: `bool`
+Default: `true`
+Run-time switchable: **No** (`PHP_INI_SYSTEM`)
+
+Enables or disables the hook into `\PDO::__construct` to swap the current time in MySQL function and keywords (e.g. `NOW()`, `CURRENT_TIMESTAMP`)
+
+#### `colopl_timeshifter.is_hook_request_time`
+
+Type: `bool`
+Default: `true`
+Run-time switchable: **No** (`PHP_INI_SYSTEM`)
+
+Selects whether to hook the $_SERVER superglobals `REQUEST_TIME` and `REQUEST_TIME_FLOAT`.
+
+#### `colopl_timeshifter.usleep_sec`
+
+Type: `int` (`int<1, max>`)
+Defalt: `1`
+Run-time switchable: **Yes** (`PHP_INI_ALL`)
+
+For a string representing time, set the number of wait microseconds to check whether it is absolute or relative time.
+
+#### `colopl_timeshifter.is_restore_per_request`
+
+Type: `bool`
+Default: `false`
+Run-time switchable: **Yes** (`PHP_INI_ALL`)
+
+Sets whether or not to unhook at the end of the request.
+
+## Functions
+
+> [!TIP]
+> Install `colopl/colopl_timeshifter` **Composer** package and use `Colopl\ColoplTimeShifter\Manager` support class instead.
+
+#### `\Colopl\ColoplTimeShifter\register_hook(\DateInterval $interval): bool`
+
+Sets the time difference to be subtracted from the current time.
+
+If the hook succeeds, it returns `true`; otherwise, it returns `false`.
+
+#### `\Colopl\ColoplTimeShifter\unregister_hook(): void`
+
+Breaks the hook.
+
+#### `\Colopl\ColoplTimeShifter\is_hooked(): bool`
+
+Check to see if the hook is done. Returns `true` if the hook is done, `false` otherwise.
+
+## License
+
+PHP License 3.01
diff --git a/build/ubuntu2204/Dockerfile b/build/ubuntu2204/Dockerfile
new file mode 100644
index 0000000..1fd8930
--- /dev/null
+++ b/build/ubuntu2204/Dockerfile
@@ -0,0 +1,12 @@
+ARG ARCH=amd64
+
+FROM ${ARCH}/ubuntu:22.04
+
+RUN apt-get update && \
+ DEBIAN_FRONTEND="noninteractive" apt-get install -y "php" "php-dev" "checkinstall"
+
+COPY ./ext /tmp/ext
+
+COPY ./build/ubuntu2204/build.sh /usr/bin/build
+
+ENTRYPOINT ["/usr/bin/build"]
diff --git a/build/ubuntu2204/build.sh b/build/ubuntu2204/build.sh
new file mode 100755
index 0000000..b3e9f50
--- /dev/null
+++ b/build/ubuntu2204/build.sh
@@ -0,0 +1,18 @@
+#!/bin/sh -eux
+
+cd "/tmp/ext"
+ echo "COLOPL PHP timeshifter extension" > "description-pak"
+ phpize
+ ./configure --with-php-config="$(which "php-config")"
+ make -j$(nproc)
+ checkinstall \
+ --pkgname="php-colopl-timeshifter" \
+ --pkglicense="PHP-3.01" \
+ --pkgversion="${VERSION}" \
+ --pkggroup="php" \
+ --maintainer="g-kudo@colopl.co.jp" \
+ --requires="php" \
+ --stripso="yes" \
+ --pakdir="/tmp/artifacts" \
+ --nodoc
+cd -
diff --git a/ci.sh b/ci.sh
new file mode 100755
index 0000000..f6bbc9c
--- /dev/null
+++ b/ci.sh
@@ -0,0 +1,108 @@
+#!/bin/sh -e
+
+case "${1}" in
+ "") ;;
+ "test") TEST_EXTENSION=1;;
+ "debug") TEST_EXTENSION_DEBUG=1;;
+ "valgrind") TEST_EXTENSION_VALGRIND=1;;
+ "msan") TEST_EXTENSION_MSAN=1;;
+ "asan") TEST_EXTENSION_ASAN=1;;
+ "ubsan") TEST_EXTENSION_UBSAN=1;;
+ *) printf "Pskel CI\nusage:\n\t\t%s\t\t: %s\n\t\t%s\t\t: %s\n\t\t%s\t: %s\n\t\t%s\t\t: %s\n\t\t%s\t\t: %s\n\t\t%s\t\t: %s\n" "test" "Test extension with pre-installed PHP binary. [bin: $(which "php")]" "debug" "Test extension with Debug Build (GCC) binary. [bin: $(which "debug-php")]" "valgrind" "Test extension with GCC binary with Valgrind. [bin: $(which "gcc-valgrind-php")]" "msan" "Test extension with Clang binary with MemorySanitizer. [bin: $(which "clang-msan-php")]" "asan" "Test extension with Clang binary with AddressSanitizer. [bin: $(which "clang-asan-php")]" "ubsan" "Test extension with Clang binary with UndefinedBehaviorSanitizer. [bin: $(which "clang-ubsan-php")]"; exit 0;;
+esac
+
+echo "[Pskel CI] BEGIN TEST"
+
+if test "${TEST_EXTENSION}" != ""; then
+ cd "/ext"
+ phpize
+ ./configure --with-php-config="$(which php-config)"
+ make clean
+ make -j"$(nproc)"
+ TEST_PHP_ARGS="--show-diff -q" make test
+ make install
+ docker-php-ext-enable "colopl_timeshifter"
+ cd "/work"
+ composer install
+ composer exec -- phpunit "tests/"
+else
+ echo "[Pskel CI] skip: TEST_EXTENSION is not set"
+fi
+
+if test "${TEST_EXTENSION_DEBUG}" != ""; then
+ cd "/ext"
+ debug-phpize
+ ./configure --with-php-config="$(which debug-php-config)"
+ make clean
+ make -j"$(nproc)"
+ TEST_PHP_ARGS="--show-diff -q" make test
+else
+ echo "[Pskel CI] skip: TEST_EXTENSION_DEBUG is not set"
+fi
+
+if test "${TEST_EXTENSION_VALGRIND}" != ""; then
+ if type "gcc-valgrind-php" > /dev/null 2>&1; then
+ cd "/ext"
+ gcc-valgrind-phpize
+ ./configure --with-php-config="$(which gcc-valgrind-php-config)"
+ make clean
+ make -j"$(nproc)"
+ TEST_PHP_ARGS="--show-diff -q -m" make test
+ else
+ echo "[Pskel CI] missing gcc-valgrind-php"
+ exit 1
+ fi
+else
+ echo "[Pskel CI] skip: TEST_EXTENSION_VALGRIND is not set"
+fi
+
+if test "${TEST_EXTENSION_MSAN}" != ""; then
+ if type "clang-msan-php" > /dev/null 2>&1; then
+ cd "/ext"
+ clang-msan-phpize
+ CC="clang" CXX="clang++" CFLAGS="-fsanitize=memory -DZEND_TRACK_ARENA_ALLOC" CPPFLAGS="-fsanitize=memory -DZEND_TRACK_ARENA_ALLOC ${CPPFLAGS}" LDFLAGS="-fsanitize=memory" ./configure --with-php-config="$(which clang-msan-php-config)"
+ make clean
+ CFLAGS="-fsanitize=memory -DZEND_TRACK_ARENA_ALLOC ${CFLAGS}" CPPFLAGS="-fsanitize=memory -DZEND_TRACK_ARENA_ALLOC ${CPPFLAGS}" LDFLAGS="-fsanitize=memory" make -j"$(nproc)"
+ TEST_PHP_ARGS="--show-diff -q --msan" make test
+ else
+ echo "[Pskel CI] missing clang-msan-php"
+ exit 1
+ fi
+else
+ echo "[Pskel CI] skip: TEST_EXTENSION_MSAN is not set"
+fi
+
+if test "${TEST_EXTENSION_ASAN}" != ""; then
+ if type "clang-asan-php" > /dev/null 2>&1; then
+ cd "/ext"
+ clang-asan-phpize
+ CC="clang" CXX="clang++" CFLAGS="-fsanitize=address -DZEND_TRACK_ARENA_ALLOC" CPPFLAGS="-fsanitize=address -DZEND_TRACK_ARENA_ALLOC ${CPPFLAGS}" LDFLAGS="-fsanitize=address" ./configure --with-php-config="$(which clang-asan-php-config)"
+ make clean
+ CFLAGS="-fsanitize=address -DZEND_TRACK_ARENA_ALLOC ${CFLAGS}" CPPFLAGS="-fsanitize=address -DZEND_TRACK_ARENA_ALLOC ${CPPFLAGS}" LDFLAGS="-fsanitize=address" make -j"$(nproc)"
+ TEST_PHP_ARGS="--show-diff -q --asan" make test
+ else
+ echo "[Pskel CI] missing clang-asan-php"
+ exit 1
+ fi
+else
+ echo "[Pskel CI] skip: TEST_EXTENSION_ASAN is not set"
+fi
+
+if test "${TEST_EXTENSION_UBSAN}" != ""; then
+ if type "clang-ubsan-php" > /dev/null 2>&1; then
+ cd "/ext"
+ clang-ubsan-phpize
+ CC="clang" CXX="clang++" CFLAGS="-fsanitize=undefined -DZEND_TRACK_ARENA_ALLOC" CPPFLAGS="-fsanitize=undefined -DZEND_TRACK_ARENA_ALLOC ${CPPFLAGS}" LDFLAGS="-fsanitize=undefined" ./configure --with-php-config="$(which clang-ubsan-php-config)"
+ make clean
+ CFLAGS="-fsanitize=undefined -DZEND_TRACK_ARENA_ALLOC ${CFLAGS}" CPPFLAGS="-fsanitize=undefined -DZEND_TRACK_ARENA_ALLOC ${CPPFLAGS}" LDFLAGS="-fsanitize=undefined" make -j"$(nproc)"
+ TEST_PHP_ARGS="--show-diff -q" make test
+ else
+ echo "[Pskel CI] missing clang-ubsan-php"
+ exit 1
+ fi
+else
+ echo "[Pskel CI] skip: TEST_EXTENSION_UBSAN is not set"
+fi
+
+echo "[Pskel CI] END TEST"
+exit 0
diff --git a/compose.yaml b/compose.yaml
new file mode 100644
index 0000000..1628d28
--- /dev/null
+++ b/compose.yaml
@@ -0,0 +1,30 @@
+services:
+ dev:
+ build:
+ context: ./
+ dockerfile: ./Dockerfile
+ cap_add:
+ - SYS_PTRACE
+ security_opt:
+ - seccomp:unconfined
+ privileged: true
+ volumes:
+ - ./ext:/usr/src/php/ext/extension:cached
+ - ./:/work:cached
+ tty: true
+ depends_on:
+ - mysql
+ - tidb
+ command: ["sleep", "infinity"]
+ mysql:
+ image: mysql:8.0
+ environment:
+ MYSQL_ROOT_PASSWORD: testing
+ MYSQL_DATABASE: testing
+ MYSQL_USER: testing
+ MYSQL_PASSWORD: testing
+ volumes:
+ - ./docker/mysql/etc/mysql/conf.d/my.cnf:/etc/mysql/conf.d/my.cnf
+ tidb:
+ image: pingcap/tidb:v7.1.1
+
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..8fb741b
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,34 @@
+{
+ "name": "colopl/colopl_timeshifter",
+ "description": "Current time modification extension wrapper library.",
+ "type": "library",
+ "require": {
+ "php": "~8.1.0 || ~8.2.0 || ~8.3.0",
+ "ext-colopl_timeshifter": "^1.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1",
+ "vimeo/psalm": "^5",
+ "phpunit/phpunit": "^10",
+ "phpstan/phpstan-phpunit": "^1",
+ "psalm/plugin-phpunit": "^0.19"
+ },
+ "license": "PHP-3.01",
+ "autoload": {
+ "psr-4": {
+ "Colopl\\ColoplTimeShifter\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Colopl\\ColoplTimeShifter\\Tests\\": "tests/"
+ }
+ },
+ "authors": [
+ {
+ "name": "Go Kudo",
+ "email": "g-kudo@colopl.co.jp"
+ }
+ ],
+ "minimum-stability": "stable"
+}
diff --git a/docker/mysql/etc/mysql/conf.d/my.cnf b/docker/mysql/etc/mysql/conf.d/my.cnf
new file mode 100644
index 0000000..e356587
--- /dev/null
+++ b/docker/mysql/etc/mysql/conf.d/my.cnf
@@ -0,0 +1,2 @@
+[mysqld]
+default-authentication-plugin=mysql_native_password
diff --git a/ext/.gitignore b/ext/.gitignore
new file mode 100644
index 0000000..f5e11c8
--- /dev/null
+++ b/ext/.gitignore
@@ -0,0 +1,44 @@
+*.lo
+*.la
+*.dep
+.libs
+acinclude.m4
+aclocal.m4
+autom4te.cache
+build
+config.guess
+config.h
+config.h.in
+config.h.in~
+config.log
+config.nice
+config.status
+config.sub
+configure~
+configure
+configure.ac
+configure.in
+include
+install-sh
+libtool
+ltmain.sh
+Makefile
+Makefile.fragments
+Makefile.global
+Makefile.objects
+missing
+mkinstalldirs
+modules
+php_test_results_*.txt
+phpt.*
+run-test-info.php
+run-tests.php
+tests/**/*.diff
+tests/**/*.out
+tests/**/*.php
+tests/**/*.exp
+tests/**/*.log
+tests/**/*.sh
+tests/**/*.db
+tests/**/*.mem
+tmp-php.ini
diff --git a/ext/.vscode/c_cpp_properties.json b/ext/.vscode/c_cpp_properties.json
new file mode 100644
index 0000000..abf07c8
--- /dev/null
+++ b/ext/.vscode/c_cpp_properties.json
@@ -0,0 +1,21 @@
+{
+ "configurations": [
+ {
+ "name": "Linux",
+ "includePath": [
+ "${workspaceFolder}/**",
+ "/usr/local/include/php",
+ "/usr/local/include/php/TSRM",
+ "/usr/local/include/php/Zend",
+ "/usr/local/include/php/ext",
+ "/usr/local/include/php/include",
+ "/usr/local/include/php/main",
+ "/usr/local/include/php/sapi"
+ ],
+ "defines": [],
+ "compilerPath": "/usr/bin/gcc",
+ "cStandard": "c99"
+ }
+ ],
+ "version": 4
+}
diff --git a/ext/.vscode/settings.json b/ext/.vscode/settings.json
new file mode 100644
index 0000000..6ef144f
--- /dev/null
+++ b/ext/.vscode/settings.json
@@ -0,0 +1,22 @@
+{
+ "files.associations": {
+ "*.phpt": "php",
+ "colopl_timeshifter_arginfo.h": "c",
+ "timelib.h": "c",
+ "php_date.h": "c",
+ "php_colopl_timeshifter.h": "c",
+ "info.h": "c",
+ "typeinfo": "cpp",
+ "hook.h": "c",
+ "mysqlnd_libmysql_compat.h": "c",
+ "system_error": "c",
+ "array": "c",
+ "functional": "c",
+ "tuple": "c",
+ "type_traits": "c",
+ "utility": "c",
+ "string_view": "c",
+ "initializer_list": "c",
+ "zend_max_execution_timer.h": "c"
+ }
+}
diff --git a/ext/colopl_timeshifter.c b/ext/colopl_timeshifter.c
new file mode 100644
index 0000000..64fbac7
--- /dev/null
+++ b/ext/colopl_timeshifter.c
@@ -0,0 +1,212 @@
+/*
+ +----------------------------------------------------------------------+
+ | COLOPL PHP TimeShifter. |
+ +----------------------------------------------------------------------+
+ | Copyright (c) COLOPL, Inc. |
+ +----------------------------------------------------------------------+
+ | This source file is subject to version 3.01 of the PHP license, |
+ | that is bundled with this package in the file LICENSE, and is |
+ | available through the world-wide-web at the following url: |
+ | http://www.php.net/license/3_01.txt |
+ | If you did not receive a copy of the PHP license and are unable to |
+ | obtain it through the world-wide-web, please send a note to |
+ | info@colopl.co.jp so we can mail you a copy immediately. |
+ +----------------------------------------------------------------------+
+ | Author: Go Kudo |
+ +----------------------------------------------------------------------+
+*/
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include "php.h"
+#include "ext/date/php_date.h"
+#include "ext/standard/info.h"
+#include "php_colopl_timeshifter.h"
+#include "colopl_timeshifter_arginfo.h"
+
+#include "shared_memory.h"
+#include "hook.h"
+
+/* True global */
+typedef struct {
+ bool is_hooked;
+ timelib_rel_time shift_interval;
+} timeshifter_global_t;
+sm_t timeshifter_global;
+
+/* Module global */
+ZEND_DECLARE_MODULE_GLOBALS(colopl_timeshifter);
+
+PHP_INI_BEGIN()
+ STD_PHP_INI_ENTRY("colopl_timeshifter.is_hook_pdo_mysql", "1", PHP_INI_SYSTEM, OnUpdateBool, is_hook_pdo_mysql, zend_colopl_timeshifter_globals, colopl_timeshifter_globals)
+ STD_PHP_INI_ENTRY("colopl_timeshifter.is_hook_request_time", "1", PHP_INI_SYSTEM, OnUpdateBool, is_hook_request_time, zend_colopl_timeshifter_globals, colopl_timeshifter_globals)
+ STD_PHP_INI_ENTRY("colopl_timeshifter.usleep_sec", "1", PHP_INI_ALL, OnUpdateLong, usleep_sec, zend_colopl_timeshifter_globals, colopl_timeshifter_globals)
+ STD_PHP_INI_ENTRY("colopl_timeshifter.is_restore_per_request", "0", PHP_INI_ALL, OnUpdateBool, is_restore_per_request, zend_colopl_timeshifter_globals, colopl_timeshifter_globals)
+PHP_INI_END()
+
+void get_shift_interval(timelib_rel_time *time) {
+ timeshifter_global_t tg;
+
+ sm_read(×hifter_global, &tg);
+ if (tg.is_hooked) {
+ memcpy(time, &tg.shift_interval, sizeof(timelib_rel_time));
+ }
+}
+
+void set_is_hooked(bool flag) {
+ timeshifter_global_t tg;
+
+ sm_read(×hifter_global, &tg);
+ if (tg.is_hooked != flag) {
+ tg.is_hooked = flag;
+ sm_write(×hifter_global, &tg);
+ }
+}
+
+bool get_is_hooked() {
+ timeshifter_global_t tg;
+
+ sm_read(×hifter_global, &tg);
+ return tg.is_hooked;
+}
+
+ZEND_FUNCTION(Colopl_ColoplTimeShifter_register_hook)
+{
+ zval *intern;
+ timeshifter_global_t tg;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_OBJECT_OF_CLASS(intern, php_date_get_interval_ce())
+ ZEND_PARSE_PARAMETERS_END();
+
+ /* Copy interval. */
+ sm_read(×hifter_global, &tg);
+ memcpy(&tg.shift_interval, Z_PHPINTERVAL_P(intern)->diff, sizeof(timelib_rel_time));
+ tg.is_hooked = true;
+ if (!sm_write(×hifter_global, &tg)) {
+ RETURN_FALSE;
+ }
+
+ if (COLOPL_TS_G(is_hook_request_time)) {
+ apply_request_time_hook();
+ }
+
+ RETURN_TRUE;
+}
+
+ZEND_FUNCTION(Colopl_ColoplTimeShifter_unregister_hook)
+{
+ set_is_hooked(false);
+}
+
+ZEND_FUNCTION(Colopl_ColoplTimeShifter_is_hooked)
+{
+ RETURN_BOOL(get_is_hooked());
+}
+
+PHP_MINIT_FUNCTION(colopl_timeshifter)
+{
+ REGISTER_INI_ENTRIES();
+
+ if (COLOPL_TS_G(is_hook_pdo_mysql) == true) {
+ register_pdo_hook();
+ }
+
+ if (!register_hooks()) {
+ return FAILURE;
+ }
+
+ if (get_is_hooked() && COLOPL_TS_G(is_hook_request_time)) {
+ apply_request_time_hook();
+ }
+
+ COLOPL_TS_G(pdo_mysql_orig_methods) = NULL;
+
+ return SUCCESS;
+}
+
+PHP_MSHUTDOWN_FUNCTION(colopl_timeshifter)
+{
+ UNREGISTER_INI_ENTRIES();
+
+ if (!unregister_hooks()) {
+ return FAILURE;
+ }
+
+ return SUCCESS;
+}
+
+PHP_RINIT_FUNCTION(colopl_timeshifter)
+{
+# if defined(ZTS) && defined(COMPILE_DL_COLOPL_TIMESHIFTER)
+ ZEND_TSRMLS_CACHE_UPDATE();
+# endif
+
+ COLOPL_TS_G(orig_request_time) = 0;
+ COLOPL_TS_G(orig_request_time_float) = 0.0;
+
+ return SUCCESS;
+}
+
+PHP_RSHUTDOWN_FUNCTION(colopl_timeshifter)
+{
+ if (COLOPL_TS_G(is_restore_per_request) && get_is_hooked()) {
+ set_is_hooked(false);
+ if (!unregister_hooks()) {
+ return FAILURE;
+ }
+ }
+
+ return SUCCESS;
+}
+
+PHP_MINFO_FUNCTION(colopl_timeshifter)
+{
+ php_info_print_table_start();
+ php_info_print_table_header(2, "colopl_timeshifter support", "enabled");
+ php_info_print_table_end();
+}
+
+PHP_GINIT_FUNCTION(colopl_timeshifter)
+{
+ timeshifter_global_t tg;
+
+# if defined(ZTS) && defined(COMPILE_DL_COLOPL_TIMESHIFTER)
+ ZEND_TSRMLS_CACHE_UPDATE();
+# endif
+
+ sm_init(×hifter_global, sizeof(timeshifter_global_t));
+ sm_read(×hifter_global, &tg);
+ tg.is_hooked = false;
+ sm_write(×hifter_global, &tg);
+}
+
+PHP_GSHUTDOWN_FUNCTION(colopl_timeshifter)
+{
+ sm_free(×hifter_global);
+}
+
+zend_module_entry colopl_timeshifter_module_entry = {
+ STANDARD_MODULE_HEADER,
+ "colopl_timeshifter",
+ ext_functions,
+ PHP_MINIT(colopl_timeshifter),
+ PHP_MSHUTDOWN(colopl_timeshifter),
+ PHP_RINIT(colopl_timeshifter),
+ PHP_RSHUTDOWN(colopl_timeshifter),
+ PHP_MINFO(colopl_timeshifter),
+ PHP_COLOPL_TIMESHIFTER_VERSION,
+ PHP_MODULE_GLOBALS(colopl_timeshifter),
+ PHP_GINIT(colopl_timeshifter),
+ PHP_GSHUTDOWN(colopl_timeshifter),
+ NULL,
+ STANDARD_MODULE_PROPERTIES_EX
+};
+
+#ifdef COMPILE_DL_COLOPL_TIMESHIFTER
+# ifdef ZTS
+ZEND_TSRMLS_CACHE_DEFINE()
+# endif
+ZEND_GET_MODULE(colopl_timeshifter)
+#endif
diff --git a/ext/colopl_timeshifter.stub.php b/ext/colopl_timeshifter.stub.php
new file mode 100644
index 0000000..8043483
--- /dev/null
+++ b/ext/colopl_timeshifter.stub.php
@@ -0,0 +1,9 @@
+ $ext_builddir/third_party/timelib/timelib_config.h <
+#endif
+#include
+#include
+
+#include "zend.h"
+
+#define timelib_malloc emalloc
+#define timelib_realloc erealloc
+#define timelib_calloc ecalloc
+#define timelib_strdup estrdup
+#define timelib_strndup estrndup
+#define timelib_free efree
+EOF
diff --git a/ext/hook.c b/ext/hook.c
new file mode 100644
index 0000000..531fc3d
--- /dev/null
+++ b/ext/hook.c
@@ -0,0 +1,829 @@
+/*
+ +----------------------------------------------------------------------+
+ | COLOPL PHP TimeShifter. |
+ +----------------------------------------------------------------------+
+ | Copyright (c) COLOPL, Inc. |
+ +----------------------------------------------------------------------+
+ | This source file is subject to version 3.01 of the PHP license, |
+ | that is bundled with this package in the file LICENSE, and is |
+ | available through the world-wide-web at the following url: |
+ | http://www.php.net/license/3_01.txt |
+ | If you did not receive a copy of the PHP license and are unable to |
+ | obtain it through the world-wide-web, please send a note to |
+ | info@colopl.co.jp so we can mail you a copy immediately. |
+ +----------------------------------------------------------------------+
+ | Author: Go Kudo |
+ +----------------------------------------------------------------------+
+*/
+
+#include "hook.h"
+
+#include "php.h"
+#include "php_colopl_timeshifter.h"
+#include "ext/date/php_date.h"
+#include "ext/pdo/php_pdo.h"
+#include "ext/pdo/php_pdo_driver.h"
+
+#include "third_party/timelib/timelib.h"
+
+#ifdef PHP_WIN32
+# include "win32/time.h"
+#else
+# include
+#endif
+
+static inline void apply_interval(timelib_time **time, timelib_rel_time *interval)
+{
+ timelib_time *new_time = timelib_sub(*time, interval);
+ timelib_time_dtor(*time);
+ *time = new_time;
+}
+
+#define CALL_ORIGINAL_FUNCTION_WITH_PARAMS(_name, _params, _param_count) \
+ do { \
+ zend_fcall_info *fci = ecalloc(1, sizeof(zend_fcall_info)); \
+ zend_fcall_info_cache *fcc = ecalloc(1, sizeof(zend_fcall_info_cache)); \
+ fci->size = sizeof(zend_fcall_info); \
+ fci->object = NULL; \
+ fci->retval = return_value; \
+ fci->param_count = _param_count; \
+ fci->params = _params; \
+ fci->named_params = NULL; \
+ fcc->function_handler = zend_hash_str_find_ptr(CG(function_table), #_name, strlen(#_name)); \
+ fcc->function_handler->internal_function.handler = COLOPL_TS_G(orig_##_name); \
+ fcc->called_scope = NULL; \
+ fcc->object = NULL; \
+ zend_call_function(fci, fcc); \
+ efree(fci); \
+ efree(fcc); \
+ } while (0);
+
+#define CALL_ORIGINAL_FUNCTION(name) \
+ do { \
+ COLOPL_TS_G(orig_##name)(INTERNAL_FUNCTION_PARAM_PASSTHRU); \
+ } while (0);
+
+#define CHECK_STATE(name) \
+ do { \
+ if (!get_is_hooked()) { \
+ CALL_ORIGINAL_FUNCTION(name); \
+ return; \
+ } \
+ } while (0);
+
+#define DEFINE_DT_HOOK_CONSTRUCTOR(name) \
+ static void hook_##name##_con(INTERNAL_FUNCTION_PARAMETERS) \
+ { \
+ CHECK_STATE(name##_con); \
+ \
+ CALL_ORIGINAL_FUNCTION(name##_con); \
+ \
+ zend_string *datetime = NULL; \
+ zval *timezone = NULL; \
+ php_date_obj *date = NULL; \
+ timelib_rel_time interval; \
+ \
+ ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_QUIET, 0, 2) \
+ Z_PARAM_OPTIONAL; \
+ Z_PARAM_STR_OR_NULL(datetime); \
+ Z_PARAM_OBJECT_OF_CLASS_OR_NULL(timezone, php_date_get_timezone_ce()); \
+ ZEND_PARSE_PARAMETERS_END(); \
+ \
+ date = Z_PHPDATE_P(ZEND_THIS); \
+ \
+ /* Early return if construction failed. */ \
+ if (!date || !date->time) { \
+ return; \
+ } \
+ \
+ if (datetime && is_fixed_time_str(datetime, timezone)) { \
+ return; \
+ } \
+ \
+ get_shift_interval(&interval); \
+ apply_interval(&date->time, &interval); \
+ }
+
+#define DEFINE_CREATE_FROM_FORMAT_EX(fname, name) \
+ static void hook_##fname(INTERNAL_FUNCTION_PARAMETERS) { \
+ CHECK_STATE(name); \
+ \
+ php_date_obj *date; \
+ timelib_time *time = NULL; \
+ zval *params, orig_return_value; \
+ uint32_t param_count = 0; \
+ timelib_time *current_time = get_current_timelib_time(); \
+ timelib_time *shifted_time = get_shifted_timelib_time(); \
+ \
+ CALL_ORIGINAL_FUNCTION(name); \
+ \
+ if (EG(exception) || Z_TYPE_P(return_value) == IS_FALSE) { \
+ RETURN_FALSE; \
+ } \
+ \
+ date = Z_PHPDATE_P(return_value); \
+ time = date->time; \
+ \
+ zend_parse_parameters(ZEND_NUM_ARGS(), "+", ¶ms, ¶m_count); \
+ \
+ if (memchr(Z_STRVAL(params[0]), '!', Z_STRLEN(params[0])) != NULL) { \
+ /* fixed (unix epoch) */ \
+ return; \
+ } \
+ \
+ /* Fixed check */ \
+ if (current_time->y == time->y) { \
+ time->y = shifted_time->y; \
+ } \
+ if (current_time->m == time->m) { \
+ time->m = shifted_time->m; \
+ } \
+ if (current_time->d == time->d) { \
+ time->d = shifted_time->d; \
+ } \
+ if (current_time->h == time->h) { \
+ time->h = shifted_time->h; \
+ } \
+ if (current_time->i == time->i) { \
+ time->i = shifted_time->i; \
+ } \
+ /* Maybe sometimes mistake, but not bothered. */ \
+ if (llabs(current_time->s - time->s) <= 3) { \
+ time->s = shifted_time->s; \
+ } \
+ if (llabs(current_time->us - time->us) <= 10) { \
+ time->us = shifted_time->us; \
+ } \
+ \
+ /* Apply changes */ \
+ timelib_update_ts(time, NULL); \
+ \
+ /* Clean up */ \
+ timelib_time_dtor(current_time); \
+ timelib_time_dtor(shifted_time); \
+ }
+
+#define DEFINE_CREATE_FROM_FORMAT(name) \
+ DEFINE_CREATE_FROM_FORMAT_EX(name, name);
+
+#define HOOK_CONSTRUCTOR(ce, name) \
+ do { \
+ COLOPL_TS_G(orig_##name##_con) = ce->constructor->internal_function.handler; \
+ ce->constructor->internal_function.handler = hook_##name##_con; \
+ } while (0);
+
+#define HOOK_METHOD(ce, name, method) \
+ do { \
+ zend_function *php_function_entry = zend_hash_str_find_ptr(&ce->function_table, #method, strlen(#method)); \
+ ZEND_ASSERT(php_function_entry); \
+ COLOPL_TS_G(orig_##name##_##method) = php_function_entry->internal_function.handler; \
+ php_function_entry->internal_function.handler = hook_##name##_##method; \
+ } while (0);
+
+#define HOOK_FUNCTION(name) \
+ do { \
+ zend_function *php_function_entry = zend_hash_str_find_ptr(CG(function_table), #name, strlen(#name)); \
+ ZEND_ASSERT(php_function_entry); \
+ COLOPL_TS_G(orig_##name) = php_function_entry->internal_function.handler; \
+ php_function_entry->internal_function.handler = hook_##name; \
+ } while (0);
+
+#define RESTORE_CONSTRUCTOR(ce, name) \
+ do { \
+ ZEND_ASSERT(COLOPL_TS_G(orig_##name##_con)); \
+ ce->constructor->internal_function.handler = COLOPL_TS_G(orig_##name##_con); \
+ COLOPL_TS_G(orig_##name##_con) = NULL; \
+ } while (0);
+
+#define RESTORE_METHOD(ce, name, method) \
+ do { \
+ zend_function *php_function_entry = zend_hash_str_find_ptr(&ce->function_table, #method, strlen(#method)); \
+ ZEND_ASSERT(php_function_entry); \
+ ZEND_ASSERT(COLOPL_TS_G(orig_##name##_##method)); \
+ php_function_entry->internal_function.handler = COLOPL_TS_G(orig_##name##_##method); \
+ COLOPL_TS_G(orig_##name##_##method) = NULL; \
+ } while (0);
+
+#define RESTORE_FUNCTION(name) \
+ do { \
+ zend_function *php_function_entry = zend_hash_str_find_ptr(CG(function_table), #name, strlen(#name)); \
+ ZEND_ASSERT(php_function_entry); \
+ ZEND_ASSERT(COLOPL_TS_G(orig_##name)); \
+ php_function_entry->internal_function.handler = COLOPL_TS_G(orig_##name); \
+ COLOPL_TS_G(orig_##name) = NULL; \
+ } while (0);
+
+static inline bool is_fixed_time_str(zend_string *datetime, zval *timezone)
+{
+ zval before_zv, after_zv;
+ php_date_obj *before, *after;
+ zend_class_entry *ce = php_date_get_immutable_ce();
+ bool is_fixed_time_str;
+
+ php_date_instantiate(ce, &before_zv);
+ before = Z_PHPDATE_P(&before_zv);
+ php_date_initialize(before, ZSTR_VAL(datetime), ZSTR_LEN(datetime), NULL, timezone, 0);
+
+ /*
+ * Check format is absolute.
+ * FIXME: Need more instead method.
+ */
+ usleep(((uint32_t) COLOPL_TS_G(usleep_sec)) > 0 ? (uint32_t) COLOPL_TS_G(usleep_sec) : 1);
+
+ php_date_instantiate(ce, &after_zv);
+ after = Z_PHPDATE_P(&after_zv);
+ php_date_initialize(after, ZSTR_VAL(datetime), ZSTR_LEN(datetime), NULL, timezone, 0);
+
+ is_fixed_time_str = before->time->y == after->time->y
+ && before->time->m == after->time->m
+ && before->time->d == after->time->d
+ && before->time->h == after->time->h
+ && before->time->i == after->time->i
+ && before->time->s == after->time->s
+ && before->time->us == after->time->us
+ ;
+
+ zval_ptr_dtor(&before_zv);
+ zval_ptr_dtor(&after_zv);
+
+ return is_fixed_time_str;
+}
+
+static inline timelib_time *get_current_timelib_time()
+{
+ timelib_time *t = timelib_time_ctor();
+
+ timelib_unixtime2gmt(t, php_time());
+
+ return t;
+}
+
+static inline timelib_time *get_shifted_timelib_time()
+{
+ timelib_time *t = get_current_timelib_time();
+ timelib_rel_time interval;
+
+ get_shift_interval(&interval);
+ apply_interval(&t, &interval);
+
+ return t;
+}
+
+static inline time_t get_shifted_time()
+{
+ time_t timestamp;
+ timelib_time *t = get_shifted_timelib_time();
+
+ timestamp = t->sse;
+
+ timelib_time_dtor(t);
+
+ return timestamp;
+}
+
+static inline bool pdo_time_apply(pdo_dbh_t *dbh)
+{
+ zend_string *sql;
+ char buf[1024];
+
+ if (!COLOPL_TS_G(pdo_mysql_orig_methods) || !COLOPL_TS_G(pdo_mysql_orig_methods)->doer) {
+ return false;
+ }
+
+ zend_sprintf(buf, "SET @@session.timestamp = %ld;", get_shifted_time());
+ sql = zend_string_init_fast(buf, strlen(buf));
+ COLOPL_TS_G(pdo_mysql_orig_methods)->doer(dbh, sql);
+ zend_string_release(sql);
+
+ return true;
+}
+
+static bool hook_pdo_driver_preparer(pdo_dbh_t *dbh, zend_string *sql, pdo_stmt_t *stmt, zval *driver_options)
+{
+ bool retval;
+
+ if (get_is_hooked()) {
+ pdo_time_apply(dbh);
+ }
+
+ retval = COLOPL_TS_G(pdo_mysql_orig_methods)->preparer(dbh, sql, stmt, driver_options);
+
+ return retval;
+}
+
+static zend_long hook_pdo_driver_doer(pdo_dbh_t *dbh, const zend_string *sql)
+{
+ if (get_is_hooked()) {
+ pdo_time_apply(dbh);
+ }
+
+ return COLOPL_TS_G(pdo_mysql_orig_methods)->doer(dbh, sql);
+}
+
+static void hook_pdo_con(INTERNAL_FUNCTION_PARAMETERS)
+{
+ CHECK_STATE(pdo_con);
+
+ pdo_dbh_t *dbh = Z_PDO_DBH_P(ZEND_THIS);
+
+ CALL_ORIGINAL_FUNCTION(pdo_con);
+
+ if (!dbh->driver ||
+ strncmp(dbh->driver->driver_name, "mysql", 5) == 0 ||
+ dbh->methods != &COLOPL_TS_G(hooked_mysql_driver_methods)
+ ) {
+ if (!COLOPL_TS_G(pdo_mysql_orig_methods)) {
+ /* Check pdo_mysql driver. */
+ if (!dbh->methods) {
+ return;
+ }
+
+ /* Copy original methods struct. */
+ COLOPL_TS_G(pdo_mysql_orig_methods) = dbh->methods;
+ memcpy(&COLOPL_TS_G(hooked_mysql_driver_methods), dbh->methods, sizeof(struct pdo_dbh_methods));
+
+ /* Override function pointer. */
+ COLOPL_TS_G(hooked_mysql_driver_methods).preparer = hook_pdo_driver_preparer;
+ COLOPL_TS_G(hooked_mysql_driver_methods).doer = hook_pdo_driver_doer;
+ }
+
+ /* Override MySQL specific driver methods pointer. */
+ dbh->methods = &COLOPL_TS_G(hooked_mysql_driver_methods);
+ }
+}
+
+static inline void mktime_common(INTERNAL_FUNCTION_PARAMETERS, zend_long timestamp)
+{
+ zend_long hou, min, sec, mon, day, yea;
+ bool min_is_null = true, sec_is_null = true, mon_is_null = true, day_is_null = true, yea_is_null = true;
+ timelib_time *t = timelib_time_ctor();
+ timelib_rel_time interval;
+
+ timelib_unixtime2gmt(t, timestamp);
+ get_shift_interval(&interval);
+ apply_interval(&t, &interval);
+
+ ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_QUIET, 1, 6)
+ Z_PARAM_LONG(hou)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_LONG_OR_NULL(min, min_is_null)
+ Z_PARAM_LONG_OR_NULL(sec, sec_is_null)
+ Z_PARAM_LONG_OR_NULL(mon, mon_is_null)
+ Z_PARAM_LONG_OR_NULL(day, day_is_null)
+ Z_PARAM_LONG_OR_NULL(yea, yea_is_null)
+ ZEND_PARSE_PARAMETERS_END();
+
+ if (!min_is_null) {
+ t->i = min;
+ }
+
+ if (!sec_is_null) {
+ t->s = sec;
+ }
+
+ if (!mon_is_null) {
+ t->m = mon;
+ }
+
+ if (!day_is_null) {
+ t->d = day;
+ }
+
+ if (!yea_is_null) {
+ if (yea >= 0 && yea < 70) {
+ yea += 2000;
+ } else if (yea >= 70 && yea <= 100) {
+ yea += 1900;
+ }
+ t->y = yea;
+ }
+
+ RETVAL_LONG(t->sse);
+ timelib_time_dtor(t);
+}
+
+static inline void date_common(INTERNAL_FUNCTION_PARAMETERS, int localtime)
+{
+ zend_string *format;
+ zend_long ts;
+ bool ts_is_null = true;
+
+ ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_QUIET, 1, 2)
+ Z_PARAM_STR(format)
+ Z_PARAM_OPTIONAL;
+ Z_PARAM_LONG_OR_NULL(ts, ts_is_null)
+ ZEND_PARSE_PARAMETERS_END();
+
+ if (ts_is_null) {
+ ts = get_shifted_time();
+ }
+
+ RETVAL_STR(php_format_date(ZSTR_VAL(format), ZSTR_LEN(format), ts, localtime));
+}
+
+static inline void date_create_common(INTERNAL_FUNCTION_PARAMETERS, zend_class_entry *ce)
+{
+ zval *timezone_object = NULL;
+ zend_string *time_str = NULL;
+ php_date_obj *date = NULL;
+ timelib_rel_time interval;
+
+ ZEND_PARSE_PARAMETERS_START(0, 2)
+ Z_PARAM_OPTIONAL;
+ Z_PARAM_STR(time_str)
+ Z_PARAM_OBJECT_OF_CLASS_OR_NULL(timezone_object, php_date_get_timezone_ce())
+ ZEND_PARSE_PARAMETERS_END();
+
+ php_date_instantiate(ce, return_value);
+ if (!php_date_initialize(
+ Z_PHPDATE_P(return_value),
+ (!time_str ? NULL : ZSTR_VAL(time_str)),
+ (!time_str ? 0 : ZSTR_LEN(time_str)),
+ NULL,
+ timezone_object,
+ 0
+ )) {
+ zval_ptr_dtor(return_value);
+ RETVAL_FALSE;
+ }
+
+ if (time_str && is_fixed_time_str(time_str, timezone_object)) {
+ return;
+ }
+
+ get_shift_interval(&interval);
+ apply_interval(&Z_PHPDATE_P(return_value)->time, &interval);
+}
+
+DEFINE_DT_HOOK_CONSTRUCTOR(dt);
+
+DEFINE_DT_HOOK_CONSTRUCTOR(dti);
+
+static void hook_time(INTERNAL_FUNCTION_PARAMETERS)
+{
+ CHECK_STATE(time);
+
+ CALL_ORIGINAL_FUNCTION(time);
+ RETURN_LONG(get_shifted_time());
+}
+
+static void hook_mktime(INTERNAL_FUNCTION_PARAMETERS)
+{
+ CHECK_STATE(mktime);
+
+ CALL_ORIGINAL_FUNCTION(mktime);
+ mktime_common(INTERNAL_FUNCTION_PARAM_PASSTHRU, Z_LVAL_P(return_value));
+}
+
+static void hook_gmmktime(INTERNAL_FUNCTION_PARAMETERS)
+{
+ CHECK_STATE(gmmktime);
+
+ CALL_ORIGINAL_FUNCTION(gmmktime);
+ mktime_common(INTERNAL_FUNCTION_PARAM_PASSTHRU, Z_LVAL_P(return_value));
+}
+
+static void hook_date_create(INTERNAL_FUNCTION_PARAMETERS)
+{
+ CHECK_STATE(date_create);
+
+ date_create_common(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_date_get_date_ce());
+}
+
+static void hook_date_create_immutable(INTERNAL_FUNCTION_PARAMETERS)
+{
+ CHECK_STATE(date_create_immutable);
+
+ date_create_common(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_date_get_immutable_ce());
+}
+
+DEFINE_CREATE_FROM_FORMAT(date_create_from_format);
+
+DEFINE_CREATE_FROM_FORMAT(date_create_immutable_from_format);
+
+DEFINE_CREATE_FROM_FORMAT_EX(dt_createfromformat, date_create_from_format);
+
+DEFINE_CREATE_FROM_FORMAT_EX(dti_createfromformat, date_create_immutable_from_format);
+
+static void hook_date(INTERNAL_FUNCTION_PARAMETERS)
+{
+ CHECK_STATE(date);
+
+ date_common(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1);
+}
+
+static void hook_gmdate(INTERNAL_FUNCTION_PARAMETERS)
+{
+ CHECK_STATE(gmdate);
+
+ date_common(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0);
+}
+
+static void hook_idate(INTERNAL_FUNCTION_PARAMETERS)
+{
+ CHECK_STATE(idate);
+
+ zend_string *format;
+ zend_long ts;
+ bool ts_is_null = 1;
+
+ if (Z_TYPE_P(return_value) == IS_FALSE) {
+ return;
+ }
+
+ ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_QUIET,1, 2)
+ Z_PARAM_STR(format)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_LONG_OR_NULL(ts, ts_is_null)
+ ZEND_PARSE_PARAMETERS_END();
+
+ if (ts_is_null) {
+ ts = get_shifted_time();
+ }
+
+ RETURN_LONG(php_idate(ZSTR_VAL(format)[0], ts, 0));
+}
+
+static void hook_getdate(INTERNAL_FUNCTION_PARAMETERS)
+{
+ CHECK_STATE(getdate);
+
+ zend_long timestamp;
+ bool timestamp_is_null = true;
+
+ ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_QUIET, 0, 1)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_LONG_OR_NULL(timestamp, timestamp_is_null)
+ ZEND_PARSE_PARAMETERS_END();
+
+ if (!timestamp_is_null) {
+ return;
+ }
+
+ /* Call original function with timestamp params. */
+ zval params[1];
+ ZVAL_LONG(¶ms[0], get_shifted_time());
+ CALL_ORIGINAL_FUNCTION_WITH_PARAMS(getdate, params, 1);
+}
+
+static void hook_localtime(INTERNAL_FUNCTION_PARAMETERS)
+{
+ CHECK_STATE(localtime);
+
+ zend_long timestamp;
+ bool timestamp_is_null = true, associative = false;
+
+ ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_QUIET, 0, 2)
+ Z_PARAM_OPTIONAL;
+ Z_PARAM_LONG_OR_NULL(timestamp, timestamp_is_null);
+ Z_PARAM_BOOL(associative);
+ ZEND_PARSE_PARAMETERS_END();
+
+ /* Call original function with params. */
+ zval params[2];
+ ZVAL_LONG(¶ms[0], get_shifted_time());
+ ZVAL_BOOL(¶ms[1], associative);
+ CALL_ORIGINAL_FUNCTION_WITH_PARAMS(localtime, params, 2);
+}
+
+static void hook_strtotime(INTERNAL_FUNCTION_PARAMETERS)
+{
+ CHECK_STATE(strtotime);
+
+ zend_string *times, *times_lower;
+ zend_long preset_ts;
+ bool preset_ts_is_null = true;
+
+ ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_QUIET, 1, 2)
+ Z_PARAM_STR(times);
+ Z_PARAM_OPTIONAL;
+ Z_PARAM_LONG_OR_NULL(preset_ts, preset_ts_is_null);
+ ZEND_PARSE_PARAMETERS_END();
+
+ /* "now" special case */
+ times_lower = zend_string_tolower(times);
+ if (strncmp(ZSTR_VAL(times_lower), "now", 3) == 0) {
+ zend_string_release(times_lower);
+ RETURN_LONG((zend_long) get_shifted_time());
+ }
+ zend_string_release(times_lower);
+
+ if (!preset_ts_is_null || is_fixed_time_str(times, NULL) ) {
+ CALL_ORIGINAL_FUNCTION(strtotime);
+ return;
+ }
+
+ /* Call original function with params. */
+ zval *params = NULL;
+ uint32_t param_count = 0;
+ zend_parse_parameters(ZEND_NUM_ARGS(), "+", ¶ms, ¶m_count);
+ ZVAL_LONG(¶ms[1], get_shifted_time());
+ CALL_ORIGINAL_FUNCTION_WITH_PARAMS(strtotime, params, param_count);
+
+ /* Apply interval. */
+ timelib_time *t = timelib_time_ctor();
+ timelib_rel_time interval;
+ timelib_unixtime2gmt(t, Z_LVAL_P(return_value));
+ get_shift_interval(&interval);
+ apply_interval(&t, &interval);
+ RETVAL_LONG(timelib_date_to_int(t, NULL));
+ timelib_time_dtor(t);
+}
+
+#if HAVE_GETTIMEOFDAY
+static inline void gettimeofday_common(INTERNAL_FUNCTION_PARAMETERS, int mode)
+{
+ bool get_as_float = false;
+ struct timeval tp = {0};
+ timelib_time *tm = timelib_time_ctor();
+ timelib_rel_time interval;
+
+ ZEND_PARSE_PARAMETERS_START(0, 1)
+ Z_PARAM_OPTIONAL;
+ Z_PARAM_BOOL(get_as_float);
+ ZEND_PARSE_PARAMETERS_END();
+
+ if (gettimeofday(&tp, NULL)) {
+ ZEND_ASSERT(0 && "gettimeofday() can't fail");
+ }
+
+ timelib_unixtime2gmt(tm, tp.tv_sec);
+ tm->us = tp.tv_usec;
+ get_shift_interval(&interval);
+ apply_interval(&tm, &interval);
+
+ if (get_as_float) {
+ RETVAL_DOUBLE((double)(tm->sse + tm->us / 1000000.00));
+ } else {
+ if (mode) {
+ timelib_time_offset *offset;
+
+ offset = timelib_get_time_zone_info(tm->sse, get_timezone_info());
+
+ array_init(return_value);
+ add_assoc_long(return_value, "sec", tm->sse);
+ add_assoc_long(return_value, "usec", tm->us);
+
+ add_assoc_long(return_value, "minuteswest", -offset->offset / 60);
+ add_assoc_long(return_value, "dsttime", -offset->is_dst);
+
+ timelib_time_offset_dtor(offset);
+ } else {
+ RETVAL_NEW_STR(zend_strpprintf(0, "%.8F %ld", tm->us / 1000000.00, (long) tm->sse));
+ }
+ }
+
+ timelib_time_dtor(tm);
+}
+
+static void hook_microtime(INTERNAL_FUNCTION_PARAMETERS)
+{
+ CHECK_STATE(microtime);
+
+ gettimeofday_common(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0);
+}
+
+static void hook_gettimeofday(INTERNAL_FUNCTION_PARAMETERS)
+{
+ CHECK_STATE(gettimeofday);
+
+ gettimeofday_common(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1);
+}
+#endif
+
+bool register_hooks()
+{
+ /* \DateTime::__construct */
+ HOOK_CONSTRUCTOR(php_date_get_date_ce(), dt);
+
+ /* \DateTimeImmutabel::__construct */
+ HOOK_CONSTRUCTOR(php_date_get_immutable_ce(), dti);
+
+ /* \DateTime::createFromFormat */
+ HOOK_METHOD(php_date_get_date_ce(), dt, createfromformat);
+
+ /* \DateTimeImmutable::createFromFormat */
+ HOOK_METHOD(php_date_get_immutable_ce(), dti, createfromformat);
+
+ HOOK_FUNCTION(time);
+ HOOK_FUNCTION(mktime);
+ HOOK_FUNCTION(gmmktime);
+ HOOK_FUNCTION(date_create);
+ HOOK_FUNCTION(date_create_immutable);
+ HOOK_FUNCTION(date_create_from_format);
+ HOOK_FUNCTION(date_create_immutable_from_format);
+ HOOK_FUNCTION(date);
+ HOOK_FUNCTION(gmdate);
+ HOOK_FUNCTION(idate);
+ HOOK_FUNCTION(getdate);
+ HOOK_FUNCTION(localtime);
+ HOOK_FUNCTION(strtotime);
+
+#if HAVE_GETTIMEOFDAY
+ HOOK_FUNCTION(microtime);
+ HOOK_FUNCTION(gettimeofday);
+#endif
+
+ return true;
+}
+
+void register_pdo_hook()
+{
+ /* \PDO::__construct */
+ HOOK_CONSTRUCTOR(php_pdo_get_dbh_ce(), pdo);
+}
+
+bool unregister_hooks()
+{
+ /* \DateTime::__construct */
+ RESTORE_CONSTRUCTOR(php_date_get_date_ce(), dt);
+
+ /* \DateTimeImmutabel::__construct */
+ RESTORE_CONSTRUCTOR(php_date_get_immutable_ce(), dti);
+
+ /* \DateTime::createFromFormat */
+ RESTORE_METHOD(php_date_get_date_ce(), dt, createfromformat);
+
+ /* \DateTimeImmutable::createFromFormat */
+ RESTORE_METHOD(php_date_get_immutable_ce(), dti, createfromformat);
+
+ RESTORE_FUNCTION(time);
+ RESTORE_FUNCTION(mktime);
+ RESTORE_FUNCTION(gmmktime);
+ RESTORE_FUNCTION(date_create);
+ RESTORE_FUNCTION(date_create_immutable);
+ RESTORE_FUNCTION(date_create_from_format);
+ RESTORE_FUNCTION(date_create_immutable_from_format);
+ RESTORE_FUNCTION(date);
+ RESTORE_FUNCTION(gmdate);
+ RESTORE_FUNCTION(idate);
+ RESTORE_FUNCTION(getdate);
+ RESTORE_FUNCTION(localtime);
+ RESTORE_FUNCTION(strtotime);
+
+#if HAVE_GETTIMEOFDAY
+ RESTORE_FUNCTION(microtime);
+ RESTORE_FUNCTION(gettimeofday);
+#endif
+
+ return true;
+}
+
+void apply_request_time_hook()
+{
+ zval *globals_server, *request_time, *request_time_float;
+ timelib_time *t;
+ timelib_rel_time interval;
+
+ globals_server = zend_hash_str_find(&EG(symbol_table), "_SERVER", strlen("_SERVER"));
+
+ if (!globals_server || Z_TYPE_P(globals_server) != IS_ARRAY) {
+ /* $_SERVER not defined */
+ return;
+ }
+
+ request_time = zend_hash_str_find(Z_ARR_P(globals_server), "REQUEST_TIME", strlen("REQUEST_TIME"));
+ request_time_float = zend_hash_str_find(Z_ARR_P(globals_server), "REQUEST_TIME_FLOAT", strlen("REQUEST_TIME_FLOAT"));
+
+ /* Get original request time at once */
+ if (COLOPL_TS_G(orig_request_time) == 0 && COLOPL_TS_G(orig_request_time_float) == 0) {
+ if (request_time_float) {
+ COLOPL_TS_G(orig_request_time_float) = Z_DVAL_P(request_time_float);
+ } else if (request_time) {
+ COLOPL_TS_G(orig_request_time) = Z_LVAL_P(request_time);
+ } else {
+ /* Missing REQUEST_TIME or REQUEST_TIME_FLOAT */
+ return;
+ }
+ }
+
+ if (COLOPL_TS_G(orig_request_time_float) != 0) {
+ timelib_sll ts = (timelib_sll) COLOPL_TS_G(orig_request_time_float);
+ timelib_sll tus = (timelib_sll) ((COLOPL_TS_G(orig_request_time_float) - ts) * 1e6);
+
+ t = timelib_time_ctor();
+ timelib_unixtime2gmt(t, ts);
+ t->us = tus;
+ timelib_update_ts(t, NULL);
+ } else if (COLOPL_TS_G(orig_request_time) != 0) {
+ t = timelib_time_ctor();
+ timelib_unixtime2gmt(t, (timelib_sll) COLOPL_TS_G(orig_request_time));
+ } else {
+ /* REQUEST_TIME or REQUEST_TIME_FLOAT not found */
+ return;
+ }
+
+ /* Apply interval. */
+ get_shift_interval(&interval);
+ apply_interval(&t, &interval);
+
+ if (request_time) {
+ ZVAL_LONG(request_time, (zend_long) t->sse);
+ }
+
+ if (request_time_float) {
+ ZVAL_DOUBLE(request_time_float, ((double) t->sse + ((double) t->us / 1000000.0)));
+ }
+
+ timelib_time_dtor(t);
+}
diff --git a/ext/hook.h b/ext/hook.h
new file mode 100644
index 0000000..8054716
--- /dev/null
+++ b/ext/hook.h
@@ -0,0 +1,29 @@
+/*
+ +----------------------------------------------------------------------+
+ | COLOPL PHP TimeShifter. |
+ +----------------------------------------------------------------------+
+ | Copyright (c) COLOPL, Inc. |
+ +----------------------------------------------------------------------+
+ | This source file is subject to version 3.01 of the PHP license, |
+ | that is bundled with this package in the file LICENSE, and is |
+ | available through the world-wide-web at the following url: |
+ | http://www.php.net/license/3_01.txt |
+ | If you did not receive a copy of the PHP license and are unable to |
+ | obtain it through the world-wide-web, please send a note to |
+ | info@colopl.co.jp so we can mail you a copy immediately. |
+ +----------------------------------------------------------------------+
+ | Author: Go Kudo |
+ +----------------------------------------------------------------------+
+*/
+#ifndef HOOK_H
+# define HOOK_H
+
+# include "php.h"
+# include "shared_memory.h"
+
+bool register_hooks();
+void register_pdo_hook();
+bool unregister_hooks();
+void apply_request_time_hook();
+
+#endif /* HOOK_H */
diff --git a/ext/php_colopl_timeshifter.h b/ext/php_colopl_timeshifter.h
new file mode 100644
index 0000000..3f953b2
--- /dev/null
+++ b/ext/php_colopl_timeshifter.h
@@ -0,0 +1,82 @@
+/*
+ +----------------------------------------------------------------------+
+ | COLOPL PHP TimeShifter. |
+ +----------------------------------------------------------------------+
+ | Copyright (c) COLOPL, Inc. |
+ +----------------------------------------------------------------------+
+ | This source file is subject to version 3.01 of the PHP license, |
+ | that is bundled with this package in the file LICENSE, and is |
+ | available through the world-wide-web at the following url: |
+ | http://www.php.net/license/3_01.txt |
+ | If you did not receive a copy of the PHP license and are unable to |
+ | obtain it through the world-wide-web, please send a note to |
+ | info@colopl.co.jp so we can mail you a copy immediately. |
+ +----------------------------------------------------------------------+
+ | Author: Go Kudo |
+ +----------------------------------------------------------------------+
+*/
+#ifndef PHP_COLOPL_TIMESHIFTER_H
+# define PHP_COLOPL_TIMESHIFTER_H
+
+# include "ext/date/php_date.h"
+# include "ext/pdo/php_pdo_driver.h"
+
+# include "shared_memory.h"
+
+void get_shift_interval(timelib_rel_time *time);
+bool get_is_hooked();
+
+extern zend_module_entry colopl_timeshifter_module_entry;
+# define phpext_colopl_timeshifter_ptr &colopl_timeshifter_module_entry
+
+# define PHP_COLOPL_TIMESHIFTER_VERSION "1.0.0"
+
+ZEND_BEGIN_MODULE_GLOBALS(colopl_timeshifter)
+ struct pdo_dbh_methods hooked_mysql_driver_methods;
+ const struct pdo_dbh_methods *pdo_mysql_orig_methods;
+ zif_handler orig_pdo_con; /* \PDO::__construct */
+ zif_handler orig_dt_con; /* \DateTime::__construct() */
+ zif_handler orig_dt_createfromformat; /* \DateTime::createFromFormat() */
+ zif_handler orig_dti_con; /* \DateTimeImmutable::__construct() */
+ zif_handler orig_dti_createfromformat; /* \DateTimeImmutable::createFromFormat() */
+ zif_handler orig_time;
+ zif_handler orig_mktime;
+ zif_handler orig_gmmktime;
+ zif_handler orig_date_create;
+ zif_handler orig_date_create_immutable;
+ zif_handler orig_date_create_from_format;
+ zif_handler orig_date_create_immutable_from_format;
+ zif_handler orig_date;
+ zif_handler orig_gmdate;
+ zif_handler orig_idate;
+ zif_handler orig_getdate;
+ zif_handler orig_localtime;
+ zif_handler orig_strtotime;
+# if HAVE_GETTIMEOFDAY
+ zif_handler orig_microtime;
+ zif_handler orig_gettimeofday;
+# endif
+ zend_long orig_request_time;
+ double orig_request_time_float;
+ zend_long usleep_sec;
+ bool is_restore_per_request;
+ bool is_hook_pdo_mysql;
+ bool is_hook_request_time;
+ZEND_END_MODULE_GLOBALS(colopl_timeshifter)
+
+ZEND_EXTERN_MODULE_GLOBALS(colopl_timeshifter)
+
+# define COLOPL_TS_G(v) ZEND_MODULE_GLOBALS_ACCESSOR(colopl_timeshifter, v)
+
+PHP_MINIT_FUNCTION(colopl_timeshifter);
+PHP_MSHUTDOWN_FUNCTION(colopl_timeshifter);
+PHP_RINIT_FUNCTION(colopl_timeshifter);
+PHP_RSHUTDOWN_FUNCTION(colopl_timeshifter);
+PHP_MINFO_FUNCTION(colopl_timeshifter);
+/* PHP_GINIT_FUNCTION(colopl_timeshifter); */
+
+# if defined(ZTS) && defined(COMPILE_DL_COLOPL_TIMESHIFTER)
+ZEND_TSRMLS_CACHE_EXTERN()
+# endif
+
+#endif /* PHP_COLOPL_TIMESHIFTER_H */
diff --git a/ext/shared_memory.c b/ext/shared_memory.c
new file mode 100644
index 0000000..9eade0b
--- /dev/null
+++ b/ext/shared_memory.c
@@ -0,0 +1,72 @@
+/*
+ +----------------------------------------------------------------------+
+ | COLOPL PHP TimeShifter. |
+ +----------------------------------------------------------------------+
+ | Copyright (c) COLOPL, Inc. |
+ +----------------------------------------------------------------------+
+ | This source file is subject to version 3.01 of the PHP license, |
+ | that is bundled with this package in the file LICENSE, and is |
+ | available through the world-wide-web at the following url: |
+ | http://www.php.net/license/3_01.txt |
+ | If you did not receive a copy of the PHP license and are unable to |
+ | obtain it through the world-wide-web, please send a note to |
+ | info@colopl.co.jp so we can mail you a copy immediately. |
+ +----------------------------------------------------------------------+
+ | Author: Go Kudo |
+ +----------------------------------------------------------------------+
+*/
+
+#include "php.h"
+#include "shared_memory.h"
+
+bool sm_init(sm_t *sm, size_t size) {
+ sm->size = size;
+
+ if ((sm->data = mmap(NULL, sm->size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0)) == MAP_FAILED) {
+ return false;
+ }
+
+ if (sem_init(&sm->semaphore, 1, 1) != 0) {
+ return false;
+ }
+
+ return sm;
+}
+
+void sm_read(sm_t *sm, void *dest) {
+ memcpy(dest, sm->data, sm->size);
+}
+
+bool sm_write(sm_t *sm, void *src) {
+ if (!sm->data) {
+ return false;
+ }
+
+ if (sem_wait(&sm->semaphore) != 0) {
+ return false;
+ }
+
+ memcpy(sm->data, src, sm->size);
+
+ if (sem_post(&sm->semaphore) != 0) {
+ return false;
+ }
+
+ return true;
+}
+
+bool sm_free(sm_t *sm) {
+ if (!sm->data) {
+ return false;
+ }
+
+ if (munmap(sm->data, sm->size) != 0) {
+ return false;
+ }
+
+ if (sem_destroy(&sm->semaphore) != 0) {
+ return false;
+ }
+
+ return true;
+}
diff --git a/ext/shared_memory.h b/ext/shared_memory.h
new file mode 100644
index 0000000..653ac4e
--- /dev/null
+++ b/ext/shared_memory.h
@@ -0,0 +1,36 @@
+/*
+ +----------------------------------------------------------------------+
+ | COLOPL PHP TimeShifter. |
+ +----------------------------------------------------------------------+
+ | Copyright (c) COLOPL, Inc. |
+ +----------------------------------------------------------------------+
+ | This source file is subject to version 3.01 of the PHP license, |
+ | that is bundled with this package in the file LICENSE, and is |
+ | available through the world-wide-web at the following url: |
+ | http://www.php.net/license/3_01.txt |
+ | If you did not receive a copy of the PHP license and are unable to |
+ | obtain it through the world-wide-web, please send a note to |
+ | info@colopl.co.jp so we can mail you a copy immediately. |
+ +----------------------------------------------------------------------+
+ | Author: Go Kudo |
+ +----------------------------------------------------------------------+
+*/
+#ifndef SHARED_MEMORY_H
+# define SHARED_MEMORY_H
+
+# include "php.h"
+# include
+# include
+
+typedef struct {
+ void *data;
+ size_t size;
+ sem_t semaphore;
+} sm_t;
+
+bool sm_init(sm_t *sm, size_t size);
+void sm_read(sm_t *sm, void *dest);
+bool sm_write(sm_t *sm, void *src);
+bool sm_free(sm_t *sm);
+
+#endif /* SHARED_MEMORY_H */
diff --git a/ext/tests/classes/datetime_construct.phpt b/ext/tests/classes/datetime_construct.phpt
new file mode 100644
index 0000000..c82840b
--- /dev/null
+++ b/ext/tests/classes/datetime_construct.phpt
@@ -0,0 +1,25 @@
+--TEST--
+Check DateTime::__construct()
+--EXTENSIONS--
+colopl_timeshifter
+--FILE--
+= $before_now || $before_static != $after_static) {
+ die('failure');
+}
+
+die('success');
+?>
+--EXPECT--
+success
diff --git a/ext/tests/classes/datetime_create_from_format.phpt b/ext/tests/classes/datetime_create_from_format.phpt
new file mode 100644
index 0000000..88d5ebc
--- /dev/null
+++ b/ext/tests/classes/datetime_create_from_format.phpt
@@ -0,0 +1,33 @@
+--TEST--
+Check DateTime::createFromFormat()
+--EXTENSIONS--
+colopl_timeshifter
+--FILE--
+diff($before_now);
+
+if (!$before_now instanceof \DateTime || !$before_static instanceof \DateTime || !$after_now instanceof \DateTime || !$after_static instanceof \DateTime) {
+ die('failed');
+}
+
+if ($after_now != $before_now && $interval->y === 3 && $interval->invert === 0) {
+ die('success');
+}
+
+die('failed');
+?>
+--EXPECT--
+success
diff --git a/ext/tests/classes/datetime_immutable_construct.phpt b/ext/tests/classes/datetime_immutable_construct.phpt
new file mode 100644
index 0000000..0abf1dc
--- /dev/null
+++ b/ext/tests/classes/datetime_immutable_construct.phpt
@@ -0,0 +1,25 @@
+--TEST--
+Check DateTimeImmutable::__construct()
+--EXTENSIONS--
+colopl_timeshifter
+--FILE--
+= $before_now || $before_static != $after_static) {
+ die('failure');
+}
+
+die('success');
+?>
+--EXPECT--
+success
diff --git a/ext/tests/classes/datetime_immutable_create_from_format.phpt b/ext/tests/classes/datetime_immutable_create_from_format.phpt
new file mode 100644
index 0000000..6a8106a
--- /dev/null
+++ b/ext/tests/classes/datetime_immutable_create_from_format.phpt
@@ -0,0 +1,33 @@
+--TEST--
+Check DateTimeImmutable::createFromFormat()
+--EXTENSIONS--
+colopl_timeshifter
+--FILE--
+diff($before_now);
+
+if (!$before_now instanceof \DateTimeImmutable || !$before_static instanceof \DateTimeImmutable || !$after_now instanceof \DateTimeImmutable || !$after_static instanceof \DateTimeImmutable) {
+ die('failed');
+}
+
+if ($after_now != $before_now && $interval->y === 3 && $interval->invert === 0) {
+ die('success');
+}
+
+die('failed');
+?>
+--EXPECT--
+success
diff --git a/ext/tests/classes/pdo/mysql/exec_doer.phpt b/ext/tests/classes/pdo/mysql/exec_doer.phpt
new file mode 100644
index 0000000..0ebc945
--- /dev/null
+++ b/ext/tests/classes/pdo/mysql/exec_doer.phpt
@@ -0,0 +1,41 @@
+--TEST--
+Check PDO MySQL (doer)
+--EXTENSIONS--
+colopl_timeshifter
+pdo
+pdo_mysql
+mysqlnd
+--INI--
+colopl_timeshifter.is_hook_pdo_mysql=1
+--FILE--
+exec('DROP TABLE IF EXISTS testing;');
+$pdo->exec('CREATE TABLE IF NOT EXISTS testing (
+ id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ date DATETIME NOT NULL
+);');
+$pdo->exec('INSERT INTO testing (date) VALUES (NOW());');
+
+$after = new \DateTimeImmutable(
+ $pdo->query('SELECT date FROM testing ORDER BY id DESC LIMIT 1;')->fetch(\PDO::FETCH_NUM)[0]
+);
+
+$interval = $after->diff($before);
+
+if ($after < $before && 4 > $interval->days && $interval->days >= 2 && $interval->invert === 0) {
+ die('success');
+}
+
+die('failed');
+?>
+--EXPECT--
+success
diff --git a/ext/tests/classes/pdo/mysql/exec_doer_tidb.phpt b/ext/tests/classes/pdo/mysql/exec_doer_tidb.phpt
new file mode 100644
index 0000000..6d3cb68
--- /dev/null
+++ b/ext/tests/classes/pdo/mysql/exec_doer_tidb.phpt
@@ -0,0 +1,41 @@
+--TEST--
+Check PDO MySQL (doer, TiDB)
+--EXTENSIONS--
+colopl_timeshifter
+pdo
+pdo_mysql
+mysqlnd
+--FILE--
+exec('CREATE DATABASE IF NOT EXISTS testing;');
+$pdo->exec('USE testing;');
+$pdo->exec('DROP TABLE IF EXISTS testing;');
+$pdo->exec('CREATE TABLE IF NOT EXISTS testing (
+ id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ date DATETIME NOT NULL
+);');
+$pdo->exec('INSERT INTO testing (date) VALUES (NOW());');
+
+$after = new \DateTimeImmutable(
+ $pdo->query('SELECT date FROM testing ORDER BY id DESC LIMIT 1;')->fetch(\PDO::FETCH_NUM)[0]
+);
+
+$interval = $after->diff($before);
+
+if ($after < $before && 4 > $interval->days && $interval->days >= 2 && $interval->invert === 0) {
+ die('success');
+}
+
+die('failed');
+?>
+--EXPECT--
+success
diff --git a/ext/tests/classes/pdo/mysql/query_preparer.phpt b/ext/tests/classes/pdo/mysql/query_preparer.phpt
new file mode 100644
index 0000000..a6db882
--- /dev/null
+++ b/ext/tests/classes/pdo/mysql/query_preparer.phpt
@@ -0,0 +1,53 @@
+--TEST--
+Check PDO MySQL (preparer)
+--EXTENSIONS--
+colopl_timeshifter
+pdo
+pdo_mysql
+mysqlnd
+--INI--
+colopl_timeshifter.is_hook_pdo_mysql=1
+--FILE--
+query('
+ SELECT NOW() AS "now",
+ CURRENT_TIMESTAMP AS "current_timestamp",
+ CURRENT_TIMESTAMP() AS "current_timestamp_fun",
+ UTC_TIMESTAMP() AS "utc_timestamp";
+', \PDO::FETCH_ASSOC)->fetch() as $result) {
+ $after = new \DateTimeImmutable($result);
+
+ if ($after->getTimestamp() >= $before->getTimestamp()) {
+ die('failure');
+ }
+
+ $interval = $after->diff($before);
+ if ($after < $before && 4 > $interval->days && $interval->days >= 2 && $interval->invert === 0) {
+ } else {
+ die('failure');
+ }
+}
+
+/* UNIX_TIMESTAMP() */
+$after = new \DateTimeImmutable('@' . $pdo->query('SELECT UNIX_TIMESTAMP() AS "unix_timestamp";')->fetch()[0]);
+$interval = $after->diff($before);
+if ($after < $before && 4 > $interval->days && $interval->days >= 2 && $interval->invert === 0) {
+} else {
+ die('failure');
+}
+
+die('success');
+
+?>
+--EXPECT--
+success
diff --git a/ext/tests/classes/pdo/mysql/query_preparer_tidb.phpt b/ext/tests/classes/pdo/mysql/query_preparer_tidb.phpt
new file mode 100644
index 0000000..9923f79
--- /dev/null
+++ b/ext/tests/classes/pdo/mysql/query_preparer_tidb.phpt
@@ -0,0 +1,53 @@
+--TEST--
+Check PDO MySQL (preparer, TiDB)
+--EXTENSIONS--
+colopl_timeshifter
+pdo
+pdo_mysql
+mysqlnd
+--FILE--
+exec('CREATE DATABASE IF NOT EXISTS testing;');
+$pdo->exec('USE testing;');
+
+/* NOW(), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP(), UTC_TIMESTAMP() */
+foreach ($pdo->query('
+ SELECT NOW() AS "now",
+ CURRENT_TIMESTAMP AS "current_timestamp",
+ CURRENT_TIMESTAMP() AS "current_timestamp_fun",
+ UTC_TIMESTAMP() AS "utc_timestamp";
+', \PDO::FETCH_ASSOC)->fetch() as $result) {
+ $after = new \DateTimeImmutable($result);
+
+ if ($after->getTimestamp() >= $before->getTimestamp()) {
+ die('failure');
+ }
+
+ $interval = $after->diff($before);
+ if ($after < $before && 4 > $interval->days && $interval->days >= 2 && $interval->invert === 0) {
+ } else {
+ die('failure');
+ }
+}
+
+/* UNIX_TIMESTAMP() */
+$after = new \DateTimeImmutable('@' . $pdo->query('SELECT UNIX_TIMESTAMP() AS "unix_timestamp";')->fetch()[0]);
+$interval = $after->diff($before);
+if ($after < $before && 4 > $interval->days && $interval->days >= 2 && $interval->invert === 0) {
+} else {
+ die('failure');
+}
+
+die('success');
+
+?>
+--EXPECT--
+success
diff --git a/ext/tests/extension.phpt b/ext/tests/extension.phpt
new file mode 100644
index 0000000..b9b926b
--- /dev/null
+++ b/ext/tests/extension.phpt
@@ -0,0 +1,47 @@
+--TEST--
+Check if colopl_timeshifter is loaded
+--FILE--
+= $after || $hooked >= $before || $seconde_hooked >= $before) {
+ die('failure behavior');
+}
+
+die('success');
+?>
+--EXPECT--
+success
diff --git a/ext/tests/functions/date.phpt b/ext/tests/functions/date.phpt
new file mode 100644
index 0000000..1b95298
--- /dev/null
+++ b/ext/tests/functions/date.phpt
@@ -0,0 +1,25 @@
+--TEST--
+Check date()
+--EXTENSIONS--
+colopl_timeshifter
+--FILE--
+diff(new \DateTime("{$before}"));
+
+if (4 > $interval->days && $interval->days >= 2 && $interval->invert === 0) {
+ die('success');
+}
+
+die('failed');
+?>
+--EXPECT--
+success
diff --git a/ext/tests/functions/date_create.phpt b/ext/tests/functions/date_create.phpt
new file mode 100644
index 0000000..ddacd09
--- /dev/null
+++ b/ext/tests/functions/date_create.phpt
@@ -0,0 +1,32 @@
+--TEST--
+Check date_create()
+--EXTENSIONS--
+colopl_timeshifter
+--FILE--
+= $before_now || $before_static != $after_static) {
+ die('failure');
+}
+
+if (
+ !$before_now instanceof \DateTime || !$before_static instanceof \DateTime ||
+ !$after_now instanceof \DateTime || !$after_static instanceof \DateTime
+) {
+ die('failure');
+}
+
+die('success');
+?>
+--EXPECT--
+success
diff --git a/ext/tests/functions/date_create_from_format.phpt b/ext/tests/functions/date_create_from_format.phpt
new file mode 100644
index 0000000..ab8b832
--- /dev/null
+++ b/ext/tests/functions/date_create_from_format.phpt
@@ -0,0 +1,33 @@
+--TEST--
+Check date_create_from_format()
+--EXTENSIONS--
+colopl_timeshifter
+--FILE--
+diff($before_now);
+
+if (!$before_now instanceof \DateTime || !$before_static instanceof \DateTime || !$after_now instanceof \DateTime || !$after_static instanceof \DateTime) {
+ die('failed');
+}
+
+if ($after_now != $before_now && $interval->y === 3 && $interval->invert === 0) {
+ die('success');
+}
+
+die('failed');
+?>
+--EXPECT--
+success
diff --git a/ext/tests/functions/date_create_immutable.phpt b/ext/tests/functions/date_create_immutable.phpt
new file mode 100644
index 0000000..ec4eda6
--- /dev/null
+++ b/ext/tests/functions/date_create_immutable.phpt
@@ -0,0 +1,32 @@
+--TEST--
+Check date_create_immutable()
+--EXTENSIONS--
+colopl_timeshifter
+--FILE--
+= $before_now || $before_static != $after_static) {
+ die('failure');
+}
+
+if (
+ !$before_now instanceof \DateTimeImmutable || !$before_static instanceof \DateTimeImmutable ||
+ !$after_now instanceof \DateTimeImmutable || !$after_static instanceof \DateTimeImmutable
+) {
+ die('failure');
+}
+
+die('success');
+?>
+--EXPECT--
+success
diff --git a/ext/tests/functions/date_create_immutable_from_format.phpt b/ext/tests/functions/date_create_immutable_from_format.phpt
new file mode 100644
index 0000000..8c6ebc4
--- /dev/null
+++ b/ext/tests/functions/date_create_immutable_from_format.phpt
@@ -0,0 +1,33 @@
+--TEST--
+Check date_create_immutable_from_format()
+--EXTENSIONS--
+colopl_timeshifter
+--FILE--
+diff($before_now);
+
+if (!$before_now instanceof \DateTimeImmutable || !$before_static instanceof \DateTimeImmutable || !$after_now instanceof \DateTimeImmutable || !$after_static instanceof \DateTimeImmutable) {
+ die('failed');
+}
+
+if ($after_now != $before_now && $interval->y === 3 && $interval->invert === 0) {
+ die('success');
+}
+
+die('failed');
+?>
+--EXPECT--
+success
diff --git a/ext/tests/functions/getdate.phpt b/ext/tests/functions/getdate.phpt
new file mode 100644
index 0000000..b38467f
--- /dev/null
+++ b/ext/tests/functions/getdate.phpt
@@ -0,0 +1,25 @@
+--TEST--
+Check getdate()
+--EXTENSIONS--
+colopl_timeshifter
+--FILE--
+diff(new \DateTime("@{$before[0]}"));
+
+if (4 > $interval->days && $interval->days >= 2 && $interval->invert === 0) {
+ die('success');
+}
+
+die('failed');
+?>
+--EXPECT--
+success
diff --git a/ext/tests/functions/gettimeofday.phpt b/ext/tests/functions/gettimeofday.phpt
new file mode 100644
index 0000000..af07008
--- /dev/null
+++ b/ext/tests/functions/gettimeofday.phpt
@@ -0,0 +1,32 @@
+--TEST--
+Check gettimeofday()
+--EXTENSIONS--
+colopl_timeshifter
+--SKIPIF--
+
+--FILE--
+diff(new \DateTime("@{$before1['sec']}"));
+$interval2 = (new \DateTime("@{$after2}"))->diff(new \DateTime("@{$before2}"));
+
+if (4 > $interval1->days && $interval1->days >= 2 && $interval1->invert === 0 &&
+ 4 > $interval2->days && $interval2->days >= 2 && $interval2->invert === 0
+) {
+ die('success');
+}
+
+die('failed');
+?>
+--EXPECT--
+success
diff --git a/ext/tests/functions/gmdate.phpt b/ext/tests/functions/gmdate.phpt
new file mode 100644
index 0000000..bd9ee03
--- /dev/null
+++ b/ext/tests/functions/gmdate.phpt
@@ -0,0 +1,25 @@
+--TEST--
+Check gmdate()
+--EXTENSIONS--
+colopl_timeshifter
+--FILE--
+diff(new \DateTime("{$before}"));
+
+if (4 > $interval->days && $interval->days >= 2 && $interval->invert === 0) {
+ die('success');
+}
+
+die('failed');
+?>
+--EXPECT--
+success
diff --git a/ext/tests/functions/gmmktime.phpt b/ext/tests/functions/gmmktime.phpt
new file mode 100644
index 0000000..0f3be18
--- /dev/null
+++ b/ext/tests/functions/gmmktime.phpt
@@ -0,0 +1,26 @@
+--TEST--
+Check gmmktime()
+--EXTENSIONS--
+colopl_timeshifter
+--FILE--
+diff(new \DateTime("@{$before}"));
+
+if ($interval->i >= 29 && 32 > $interval->i && $interval->invert === 0) {
+ die('success');
+}
+
+die('failed');
+
+?>
+--EXPECT--
+success
diff --git a/ext/tests/functions/idate.phpt b/ext/tests/functions/idate.phpt
new file mode 100644
index 0000000..c7d0de3
--- /dev/null
+++ b/ext/tests/functions/idate.phpt
@@ -0,0 +1,25 @@
+--TEST--
+Check idate()
+--EXTENSIONS--
+colopl_timeshifter
+--FILE--
+diff(new \DateTime("@{$before}"));
+
+if (4 > $interval->days && $interval->days >= 2 && $interval->invert === 0) {
+ die('success');
+}
+
+die('failed');
+?>
+--EXPECT--
+success
diff --git a/ext/tests/functions/localtime.phpt b/ext/tests/functions/localtime.phpt
new file mode 100644
index 0000000..bd55a2b
--- /dev/null
+++ b/ext/tests/functions/localtime.phpt
@@ -0,0 +1,28 @@
+--TEST--
+Check localtime()
+--EXTENSIONS--
+colopl_timeshifter
+--FILE--
+diff(new \DateTime("{$before1[5]}-{$before1[4]}-{$before1[3]} {$before1[2]}:{$before1[1]}:{$before1[0]}"));
+$interval2 = (new \DateTime("{$after2['tm_year']}-{$after2['tm_mon']}-{$after2['tm_mday']} {$after2['tm_hour']}:{$after2['tm_min']}:{$after2['tm_sec']}"))->diff(new \DateTime("{$before2['tm_year']}-{$before2['tm_mon']}-{$before2['tm_mday']} {$before2['tm_hour']}:{$before2['tm_min']}:{$before2['tm_sec']}"));
+
+if ($interval1->days >= 2 && $interval1->invert === 0) {
+ die('success');
+}
+
+die('failed');
+?>
+--EXPECT--
+success
diff --git a/ext/tests/functions/microtime.phpt b/ext/tests/functions/microtime.phpt
new file mode 100644
index 0000000..041dadf
--- /dev/null
+++ b/ext/tests/functions/microtime.phpt
@@ -0,0 +1,32 @@
+--TEST--
+Check microtime()
+--EXTENSIONS--
+colopl_timeshifter
+--SKIPIF--
+
+--FILE--
+diff(new \DateTime("@{$before1[1]}"));
+$interval2 = (new \DateTime("@{$after2}"))->diff(new \DateTime("@{$before2}"));
+
+if (4 > $interval1->days && $interval1->days >= 2 && $interval1->invert === 0 &&
+ 4 > $interval2->days && $interval2->days >= 2 && $interval2->invert === 0
+) {
+ die('success');
+}
+
+die('failed');
+?>
+--EXPECT--
+success
diff --git a/ext/tests/functions/mktime.phpt b/ext/tests/functions/mktime.phpt
new file mode 100644
index 0000000..bc14d04
--- /dev/null
+++ b/ext/tests/functions/mktime.phpt
@@ -0,0 +1,26 @@
+--TEST--
+Check mktime()
+--EXTENSIONS--
+colopl_timeshifter
+--FILE--
+diff(new \DateTime("@{$before}"));
+
+if ($interval->i >= 29 && 32 > $interval->i && $interval->invert === 0) {
+ die('success');
+}
+
+die('failed');
+
+?>
+--EXPECT--
+success
diff --git a/ext/tests/functions/strtotime.phpt b/ext/tests/functions/strtotime.phpt
new file mode 100644
index 0000000..081866d
--- /dev/null
+++ b/ext/tests/functions/strtotime.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Check strtotime()
+--EXTENSIONS--
+colopl_timeshifter
+--FILE--
+diff(new \DateTime("{$before}"));
+
+if ($before_now != $after_now && $before_fixed === $after_fixed && 4 > $interval->days && $interval->days >= 2 && $interval->invert === 0) {
+ die('success');
+}
+
+die('failed');
+?>
+--EXPECT--
+success
diff --git a/ext/tests/functions/strtotime_extra.phpt b/ext/tests/functions/strtotime_extra.phpt
new file mode 100644
index 0000000..ea96580
--- /dev/null
+++ b/ext/tests/functions/strtotime_extra.phpt
@@ -0,0 +1,30 @@
+--TEST--
+Check strtotime() extra pattern
+--EXTENSIONS--
+colopl_timeshifter
+--FILE--
+diff($before);
+
+if ($before == $after_one || $before == $after_two) {
+ die('failed');
+}
+
+/* Note: Sometime valgrind makes flaky: $interval->y !== 2 */
+if (($interval->y > 2 && $interval->y !== 0) || $interval->invert !== 0) {
+ die('failed');
+}
+
+die('success');
+
+?>
+--EXPECT--
+success
diff --git a/ext/tests/functions/time.phpt b/ext/tests/functions/time.phpt
new file mode 100644
index 0000000..49bacb9
--- /dev/null
+++ b/ext/tests/functions/time.phpt
@@ -0,0 +1,25 @@
+--TEST--
+Check time()
+--EXTENSIONS--
+colopl_timeshifter
+--FILE--
+diff(new \DateTime("@{$before}"));
+
+if (4 > $interval->days && $interval->days >= 2 && $interval->invert === 0) {
+ die('success');
+}
+
+die('failed');
+?>
+--EXPECT--
+success
diff --git a/ext/tests/variables/request_time.phpt b/ext/tests/variables/request_time.phpt
new file mode 100644
index 0000000..4478a5a
--- /dev/null
+++ b/ext/tests/variables/request_time.phpt
@@ -0,0 +1,27 @@
+--TEST--
+Check $_SERVER['REQUEST_TIME']
+--EXTENSIONS--
+colopl_timeshifter
+--FILE--
+diff($before);
+
+if ($before == $after || $interval->y !== 1 || $interval->invert !== 0) {
+ die('failed');
+}
+
+die('success');
+
+?>
+--EXPECT--
+success
diff --git a/ext/tests/variables/request_time_fail.phpt b/ext/tests/variables/request_time_fail.phpt
new file mode 100644
index 0000000..164e249
--- /dev/null
+++ b/ext/tests/variables/request_time_fail.phpt
@@ -0,0 +1,29 @@
+--TEST--
+Check fail pattern $_SERVER['REQUEST_TIME']
+--EXTENSIONS--
+colopl_timeshifter
+--INI--
+colopl_timeshifter.is_hook_request_time=0
+--FILE--
+diff($before);
+
+if ($before == $after || $interval->y !== 1 || $interval->invert !== 0) {
+ die('failed');
+}
+
+die('success');
+
+?>
+--EXPECT--
+failed
diff --git a/ext/tests/variables/request_time_float.phpt b/ext/tests/variables/request_time_float.phpt
new file mode 100644
index 0000000..3d92308
--- /dev/null
+++ b/ext/tests/variables/request_time_float.phpt
@@ -0,0 +1,31 @@
+--TEST--
+Check $_SERVER['REQUEST_TIME_FLOAT']
+--EXTENSIONS--
+colopl_timeshifter
+--FILE--
+diff($before);
+
+if ($before == $after || $interval->y !== 1 || $interval->invert !== 0) {
+ die('failed');
+}
+
+if ($before->format('u') !== $after->format('u')) {
+ die('failed');
+}
+
+die('success');
+
+?>
+--EXPECT--
+success
diff --git a/ext/third_party/timelib b/ext/third_party/timelib
new file mode 160000
index 0000000..06dc8d1
--- /dev/null
+++ b/ext/third_party/timelib
@@ -0,0 +1 @@
+Subproject commit 06dc8d1bb22816c14cfd5a09c9fc914c2086dec9
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..9f2da6d
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,10 @@
+includes:
+ - vendor/phpstan/phpstan-phpunit/extension.neon
+parameters:
+ level: max
+ paths:
+ - src
+ - tests
+ parallel:
+ processTimeout: 1800.0
+ maximumNumberOfProcesses: 4
diff --git a/psalm.xml b/psalm.xml
new file mode 100644
index 0000000..87e9167
--- /dev/null
+++ b/psalm.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Manager.php b/src/Manager.php
new file mode 100644
index 0000000..1841281
--- /dev/null
+++ b/src/Manager.php
@@ -0,0 +1,89 @@
+diff(new \DateTimeImmutable()));
+ }
+
+ /**
+ * Set interval to the current time.
+ */
+ public static function hookDateInterval(\DateInterval $dateInterval): bool
+ {
+ if (! self::isAvailable()) {
+ return \false;
+ }
+
+ /*
+ * Calculate in days to ensure correct conversion of days
+ * when specifying a period that straddles months.
+ */
+ $actualInterval = clone $dateInterval;
+ if (is_int($actualInterval->days) && $actualInterval->days > 0 && $actualInterval->days !== $actualInterval->d) {
+ /** @psalm-suppress InaccessibleProperty */
+ $actualInterval->d = $actualInterval->days;
+ /** @psalm-suppress InaccessibleProperty */
+ $actualInterval->y = 0;
+ /** @psalm-suppress InaccessibleProperty */
+ $actualInterval->m = 0;
+ }
+
+ return \Colopl\ColoplTimeShifter\register_hook($actualInterval);
+ }
+
+ private static function checkAvailable(): void
+ {
+ self::$isAvailable = \extension_loaded('colopl_timeshifter');
+ }
+}
diff --git a/tests/ManagerTest.php b/tests/ManagerTest.php
new file mode 100644
index 0000000..b1b0b29
--- /dev/null
+++ b/tests/ManagerTest.php
@@ -0,0 +1,61 @@
+format('Y-m-d'));
+ }
+
+ public function testUnhook(): void
+ {
+ $now = new \DateTimeImmutable();
+
+ Manager::hookDateTime(new \DateTimeImmutable('1994-10-26 12:00:00'));
+
+ self::assertEquals('1994-10-26', (new \DateTimeImmutable())->format('Y-m-d'));
+
+ Manager::unhook();
+
+ self::assertEquals($now->format('Y-m-d'), (new \DateTimeImmutable())->format('Y-m-d'));
+ }
+
+ public function testHookInterval(): void
+ {
+ $now = new \DateTimeImmutable();
+ self::assertTrue(Manager::hookDateInterval(new \DateInterval('P1D')));
+ self::assertEquals($now->sub(new \DateInterval('P1D'))->format('Y-m-d'), (new \DateTimeImmutable())->format('Y-m-d'));
+ }
+
+ public function testHookDateTime(): void
+ {
+ $now = new \DateTimeImmutable();
+ $diff = (new \DateTimeImmutable('1994-10-26 12:00:00'))->diff($now);
+
+ self::assertTrue(Manager::hookDateTime(new \DateTimeImmutable('1994-10-26 12:00:00')));
+
+ self::assertEquals($diff->format('%d'), (new \DateTimeImmutable())->diff($now)->format('%d'));
+ }
+
+ protected function tearDown(): void
+ {
+ Manager::unhook();
+ \Colopl\ColoplTimeShifter\unregister_hook();
+ }
+}