Initial commit

This commit is contained in:
2026-10-18 17:01:36 +00:00
commit 91a68b82f2
2334 changed files with 646393 additions and 0 deletions
+257
View File
@@ -0,0 +1,257 @@
<?php
declare(strict_types=1);
/**
* Byte.php
*
* @since 2015-07-28
* @category Library
* @package File
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2015-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-file
*
* This file is part of tc-lib-file software library.
*/
namespace Com\Tecnick\File;
use RangeException;
/**
* Com\Tecnick\File\Byte
*
* Function to read byte-level data
*
* @since 2015-07-28
* @category Library
* @package File
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2015-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-file
*/
readonly class Byte
{
/**
* Length of the binary string in bytes.
*/
private int $length;
/**
* Initialize a new string to be processed.
*
* Values are read in place via \ord() + bitwise math: no per-read unpack()
* call and no byte-table copy, so memory stays at the size of the string.
*
* The read methods are deliberately inlined (each repeats the bounds check
* and the \ord()/shift sequence) rather than sharing private helpers: every
* userland call in PHP carries real frame-setup overhead and is not inlined
* by the engine in the interpreter, so the duplication is a measurable speed
* win on the hot read path.
*
* @param string $str String (binary) from where to extract values
*/
public function __construct(
/**
* Binary string to process
*/
protected string $str,
) {
$this->length = \strlen($str);
}
/**
* Throw for a read that failed its bounds check.
*
* Kept as a separate method so the cold error path does not bloat the
* inlined read methods; it is never reached on a successful read.
*
* @param int $offset Read start position.
* @param int $length Number of bytes requested.
*
* @throws RangeException always.
*/
private function throwOutOfBounds(int $offset, int $length): never
{
throw new RangeException(
"Out-of-bounds read at offset {$offset} (length {$length}, string length {$this->length})",
);
}
/**
* Get BYTE from string (8-bit unsigned integer).
*
* @param int $offset Point from where to read the data.
*
* @return int 8 bit value
*
* @throws RangeException if the requested read is out of bounds.
*/
public function getByte(int $offset): int
{
if ($offset < 0 || $offset >= $this->length) {
$this->throwOutOfBounds($offset, 1);
}
return \ord($this->str[$offset]);
}
/**
* Get USHORT from string (Big Endian 16-bit unsigned integer).
*
* @param int $offset Point from where to read the data
*
* @return int 16 bit value
*
* @throws RangeException if the requested read is out of bounds.
*/
public function getUShort(int $offset): int
{
if ($offset < 0 || $offset > ($this->length - 2)) {
$this->throwOutOfBounds($offset, 2);
}
return (\ord($this->str[$offset]) << 8) | \ord($this->str[$offset + 1]);
}
/**
* Get SHORT from string (Big Endian 16-bit signed integer).
*
* @param int $offset Point from where to read the data.
*
* @return int 16 bit value
*
* @throws RangeException if the requested read is out of bounds.
*/
public function getShort(int $offset): int
{
if ($offset < 0 || $offset > ($this->length - 2)) {
$this->throwOutOfBounds($offset, 2);
}
// The uint16 value
$val = (\ord($this->str[$offset]) << 8) | \ord($this->str[$offset + 1]);
// Convert unsigned 16-bit to signed (two's complement)
return ($val ^ 0x8000) - 0x8000;
}
/**
* Get UFWORD from string (Big Endian 16-bit unsigned integer).
* Alias for getUShort().
*
* @param int $offset Point from where to read the data.
*
* @return int 16 bit value
*
* @throws RangeException if the requested read is out of bounds.
*/
public function getUFWord(int $offset): int
{
if ($offset < 0 || $offset > ($this->length - 2)) {
$this->throwOutOfBounds($offset, 2);
}
return (\ord($this->str[$offset]) << 8) | \ord($this->str[$offset + 1]);
}
/**
* Get FWORD from string (Big Endian 16-bit signed integer).
* Alias for getShort().
*
* @param int $offset Point from where to read the data.
*
* @return int 16 bit value
*
* @throws RangeException if the requested read is out of bounds.
*/
public function getFWord(int $offset): int
{
if ($offset < 0 || $offset > ($this->length - 2)) {
$this->throwOutOfBounds($offset, 2);
}
// The uint16 value
$val = (\ord($this->str[$offset]) << 8) | \ord($this->str[$offset + 1]);
// Convert unsigned 16-bit to signed (two's complement)
return ($val ^ 0x8000) - 0x8000;
}
/**
* Get ULONG from string (Big Endian 32-bit unsigned integer).
*
* @param int $offset Point from where to read the data
*
* @return int 32 bit value
*
* @throws RangeException if the requested read is out of bounds.
*/
public function getULong(int $offset): int
{
if ($offset < 0 || $offset > ($this->length - 4)) {
$this->throwOutOfBounds($offset, 4);
}
return (
(\ord($this->str[$offset]) << 24)
| (\ord($this->str[$offset + 1]) << 16)
| (\ord($this->str[$offset + 2]) << 8)
| \ord($this->str[$offset + 3])
);
}
/**
* Get LONG from string (Big Endian 32-bit signed integer).
*
* @param int $offset Point from where to read the data
*
* @return int 32 bit value
*
* @throws RangeException if the requested read is out of bounds.
*/
public function getLong(int $offset): int
{
if ($offset < 0 || $offset > ($this->length - 4)) {
$this->throwOutOfBounds($offset, 4);
}
// The uint32 value
$val =
(\ord($this->str[$offset]) << 24)
| (\ord($this->str[$offset + 1]) << 16)
| (\ord($this->str[$offset + 2]) << 8)
| \ord($this->str[$offset + 3]);
// Convert unsigned 32-bit to signed (two's complement)
return ($val ^ 0x8000_0000) - 0x8000_0000;
}
/**
* Get FIXED from string (Big Endian 32-bit signed fixed-point number (16.16)).
*
* A fixed-point 16.16 number is the signed int16 integer part plus the
* unsigned 16-bit fractional part divided by 65536 = (1 << 16).
*
* @param int $offset Point from where to read the data.
*
* @return float Fixed-point value
*
* @throws RangeException if the requested read is out of bounds.
*/
public function getFixed(int $offset): float
{
if ($offset < 0 || $offset > ($this->length - 4)) {
$this->throwOutOfBounds($offset, 4);
}
// The int16 integer part
$int16 = (((\ord($this->str[$offset]) << 8) | \ord($this->str[$offset + 1])) ^ 0x8000) - 0x8000;
// Add the uint16 fractional part
return $int16 + (((\ord($this->str[$offset + 2]) << 8) | \ord($this->str[$offset + 3])) / 65_536.0);
}
}
+267
View File
@@ -0,0 +1,267 @@
<?php
declare(strict_types=1);
/**
* Cache.php
*
* @since 2011-05-23
* @category Library
* @package File
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-file
*
* This file is part of tc-lib-file software library.
*/
namespace Com\Tecnick\File;
/**
* Com\Tecnick\File\Cache
*
* File caching system with per-instance path and prefix.
* Each Cache instance maintains its own cache directory path and file prefix.
*
* @since 2011-05-23
* @category Library
* @package File
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-file
*/
class Cache
{
/**
* Pattern matching characters that are not allowed in cache filename tokens
* (prefix, type, key). Stripping them keeps generated names valid on every
* platform (notably Windows) and prevents glob metacharacters from reaching
* delete()/deleteOlderThan(). getNewFileName() and delete() must use the
* same pattern so the prefix-based glob matches the files actually created.
*/
private const SAFE_NAME_PATTERN = '/[^a-zA-Z0-9_\-]/';
/**
* Cache path (per-instance)
*
* @var string
*/
protected string $path = '';
/**
* File prefix (per-instance)
*/
protected string $prefix;
/**
* Set the file prefix (common name)
*
* @param ?string $prefix Common prefix to be used for all cache files
*/
public function __construct(?string $prefix = null)
{
$this->defineSystemCachePath();
$this->setCachePath();
$prefix ??= \rtrim(
\base64_encode(\pack('H*', \md5(\uniqid((string) \mt_rand(0, \mt_getrandmax()), true)))),
'=',
);
$safePrefix = \preg_replace(self::SAFE_NAME_PATTERN, '', \strtr($prefix, '+/', '-_')) ?? '';
$this->prefix = '_' . $safePrefix . '_';
}
/**
* Get the cache directory path
*/
public function getCachePath(): string
{
return $this->path;
}
/**
* Set the default cache directory path
*
* @param ?string $path Cache directory path; if null use the K_PATH_CACHE value
*/
public function setCachePath(?string $path = null): void
{
if ($path === null || \str_contains($path, '://') || !\is_writable($path)) {
// Normalize the fallback too: K_PATH_CACHE may be supplied by the host
// application without a trailing separator, and getNewFileName()/delete()
// concatenate $this->path directly. Without normalization the generated
// file would escape the cache directory (e.g. ".../cachedir" + "_pfx_..."
// lands next to the directory instead of inside it).
$this->path = $this->normalizePath((string) \constant('K_PATH_CACHE'));
return;
}
$this->path = $this->normalizePath($path);
}
/**
* Get the file prefix
*/
public function getFilePrefix(): string
{
return $this->prefix;
}
/**
* Returns a temporary filename for caching files.
* Throws an exception when tempnam() fails, consistent with the rest of the library.
*
* @param string $type Type of file
* @param string $key File key (used to retrieve file from cache)
*
* @return string Temporary filename
*
* @throws \Com\Tecnick\File\Exception when a temporary file cannot be created.
*/
public function getNewFileName(string $type = 'tmp', string $key = '0'): string
{
// Sanitize so the generated name matches the patterns used by delete()
// and contains no characters that are invalid in Windows filenames.
$safeType = \preg_replace(self::SAFE_NAME_PATTERN, '', $type) ?? '';
$safeKey = \preg_replace(self::SAFE_NAME_PATTERN, '', $key) ?? '';
// tempnam() atomically creates a unique, private file, but on Windows it
// keeps only the first three characters of the prefix. Create the file
// with tempnam(), then rename it to a name carrying the full prefix so
// the prefix-based glob in delete()/deleteOlderThan() works on every
// platform. The rename preserves tempnam()'s restrictive permissions.
$file = \tempnam($this->path, $this->prefix);
if ($file === false) {
throw new Exception('unable to create a temporary file in: ' . $this->path);
}
$unique = \str_replace('.', '', \uniqid('', true));
$target = $this->path . $this->prefix . $safeType . '_' . $safeKey . '_' . $unique;
$renamed = $this->withoutFsWarnings(static fn(): bool => \rename($file, $target));
return $renamed ? $target : $file;
}
/**
* Delete cached files
*
* @param ?string $type Type of files to delete
* @param ?string $key Specific file key to delete
*/
public function delete(?string $type = null, ?string $key = null): void
{
$safeType = $type !== null ? \preg_replace(self::SAFE_NAME_PATTERN, '', $type) : null;
$safeKey = $key !== null ? \preg_replace(self::SAFE_NAME_PATTERN, '', $key) : null;
$path = $this->path . $this->prefix;
if ($safeType !== null) {
$path .= $safeType . '_';
if ($safeKey !== null) {
$path .= $safeKey . '_';
}
}
$path .= '*';
$files = \glob($path);
if ($files === [] || $files === false) {
return;
}
// Suppress warnings: a file may vanish (concurrent cleanup, external
// process) between glob() and unlink(); such races are non-fatal.
$this->withoutFsWarnings(static function () use ($files): void {
foreach ($files as $file) {
\unlink($file);
}
});
}
/**
* Delete cache files older than the given number of seconds.
*
* @param int $seconds Maximum age in seconds; files whose mtime is older are removed.
*/
public function deleteOlderThan(int $seconds): void
{
$pattern = $this->path . $this->prefix . '*';
$files = \glob($pattern);
if ($files === [] || $files === false) {
return;
}
$cutoff = \time() - $seconds;
// Suppress warnings: filemtime()/unlink() may fail if a file disappears
// between glob() and the call (concurrent cleanup); such races are non-fatal.
$this->withoutFsWarnings(static function () use ($files, $cutoff): void {
foreach ($files as $file) {
$mtime = \filemtime($file);
if ($mtime !== false && $mtime < $cutoff) {
\unlink($file);
}
}
});
}
/**
* Execute a callable while suppressing expected filesystem warnings.
*
* The cache helpers race against other processes and against files that may
* vanish between glob() and the operation; those calls already signal failure
* via their return values, so the raw PHP warnings only add noise.
*
* @template T
*
* @param callable():T $callback
*
* @return T
*/
private function withoutFsWarnings(callable $callback): mixed
{
\set_error_handler(static fn(): bool => true, E_WARNING);
try {
return $callback();
} finally {
\restore_error_handler();
}
}
/**
* Set the K_PATH_CACHE constant (if not set) to the default system directory for temporary files
*/
protected function defineSystemCachePath(): void
{
if (\defined('K_PATH_CACHE')) {
return;
}
$kPathCache = \ini_get('upload_tmp_dir');
if ($kPathCache === false || $kPathCache === '') {
$kPathCache = \sys_get_temp_dir();
}
\define('K_PATH_CACHE', $this->normalizePath($kPathCache));
}
/**
* Normalize cache path
*
* @param string $path Path to normalize
*/
protected function normalizePath(string $path): string
{
$rpath = \realpath($path);
if ($rpath === false) {
return '';
}
if (!\str_ends_with($rpath, \DIRECTORY_SEPARATOR)) {
$rpath .= \DIRECTORY_SEPARATOR;
}
return $rpath;
}
}
+118
View File
@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
/**
* Dir.php
*
* @since 2015-07-28
* @category Library
* @package File
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2015-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-file
*
* This file is part of tc-lib-file software library.
*/
namespace Com\Tecnick\File;
/**
* Com\Tecnick\File\Dir
*
* Function to handle directories
*
* @since 2015-07-28
* @category Library
* @package File
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2015-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-file
*/
class Dir
{
/**
* Returns the full path of a parent directory
*
* @param string $name Name of the parent folder to search
* @param string $dir Starting directory
*
* @return string Directory name
*/
public function findParentDir(string $name, string $dir = __DIR__): string
{
$allowedBases = $this->getOpenBasedirPaths();
while ($dir !== '') {
if ($dir === \dirname($dir)) {
$dir = '';
}
$candidate = $dir . DIRECTORY_SEPARATOR . $name;
if ($this->isPathAllowed($candidate, $allowedBases) && \is_writable($candidate)) {
$dir = $candidate;
break;
}
$dir = \dirname($dir);
}
if (\substr($dir, -1) !== DIRECTORY_SEPARATOR) {
$dir .= DIRECTORY_SEPARATOR;
}
return $dir;
}
/**
* Returns the list of directories allowed by the active open_basedir restriction.
*
* An empty array means that no restriction is in effect.
*
* @return array<string> Allowed base directories without trailing separator.
*/
private function getOpenBasedirPaths(): array
{
$openBasedir = \ini_get('open_basedir');
if ($openBasedir === false || $openBasedir === '') {
return [];
}
$paths = [];
foreach (\explode(PATH_SEPARATOR, $openBasedir) as $path) {
$path = \rtrim($path, '/\\');
if ($path !== '') {
$paths[] = $path;
}
}
return $paths;
}
/**
* Tells whether the given path can be safely probed under the active open_basedir restriction.
*
* Probing a path outside the allowed list raises an open_basedir E_WARNING (which can
* corrupt the output stream or be promoted to an exception by the application error handler),
* so such paths must be skipped. When no restriction is in effect every path is allowed.
*
* @param string $path Path to check.
* @param array<string> $allowedBases Allowed base directories (empty when unrestricted).
*/
private function isPathAllowed(string $path, array $allowedBases): bool
{
if ($allowedBases === []) {
return true;
}
foreach ($allowedBases as $base) {
if ($path === $base || \str_starts_with($path, $base . DIRECTORY_SEPARATOR)) {
return true;
}
}
return false;
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/**
* Exception.php
*
* @since 2015-07-28
* @category Library
* @package File
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2015-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-file
*
* This file is part of tc-lib-file software library.
*/
namespace Com\Tecnick\File;
/**
* Com\Tecnick\File\Exception
*
* Custom Exception class
*
* @since 2015-07-28
* @category Library
* @package File
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2015-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-file
*/
class Exception extends \Exception {}
File diff suppressed because it is too large Load Diff