$id, 'hit' => false, 'wait_ms' => 0, 'generation' => null]; } try { $acquired = false; while (true) { $blocked = 0; try { $acquired = @flock($lock, LOCK_EX | LOCK_NB, $blocked); } catch (\Throwable $ignored) { $blocked = 0; } if ($acquired || !$blocked) break; self::check($deadline, $abort); usleep(20000); } self::check($deadline, $abort); $wait = max(0, (int) round((microtime(true) - $waitStarted) * 1000)); if (!$acquired) { $id = $upload(); self::check($deadline, $abort); return ['id' => $id, 'hit' => false, 'wait_ms' => $wait, 'generation' => null]; } try { $record = self::read($lock, $token); } catch (\Throwable $ignored) { $record = null; } if ($record !== null) return ['id' => $record['id'], 'hit' => true, 'wait_ms' => $wait, 'generation' => null]; $id = $upload(); // Upload exceptions are real failures, never cache fallback. self::check($deadline, $abort); if (!self::validId($id)) throw new \RuntimeException('REQUIRED_FILES_UPLOAD_FAILED'); $now = microtime(true); $generation = null; // Readers and writers hold this same stable inode's exclusive lock. // A crash during truncate/write produces a cache miss, never a partial ID. try { $candidate = bin2hex(random_bytes(16)); $data = ['v' => self::VERSION, 'token' => $token, 'id' => $id, 'created' => $now, 'expires' => $now + max(1, min(120, $ttl)), 'generation' => $candidate]; if (self::write($lock, $data)) $generation = $candidate; } catch (\Throwable $ignored) { self::clear($lock); } try { self::check($deadline, $abort); } catch (\Throwable $error) { self::clear($lock); throw $error; } return ['id' => $id, 'hit' => false, 'wait_ms' => $wait, 'generation' => $generation]; } finally { @flock($lock, LOCK_UN); fclose($lock); } } /** Drop matching records only; stable lock files are never unlinked. */ public static function drop(string $root, array $tokens, array $generations = []): void { foreach (array_unique(array_filter($tokens, static fn ($token): bool => is_string($token) && preg_match('/^[a-f0-9]{64}$/D', $token) === 1)) as $token) { try { $lock = self::open($root, $token); } catch (\Throwable $ignored) { $lock = null; } if ($lock === null) continue; try { // Invalidation must never wait on another request's active upload. if (!@flock($lock, LOCK_EX | LOCK_NB)) continue; $record = self::read($lock, $token, false); if ($record !== null && (!isset($generations[$token]) || hash_equals($generations[$token], $record['generation']))) self::clear($lock); } catch (\Throwable $ignored) { /* Invalidation is best effort; callers bypass reuse on retry. */ } finally { @flock($lock, LOCK_UN); fclose($lock); } } } private static function open(string $root, string $token) { if (preg_match('/^[a-f0-9]{64}$/D', $token) !== 1 || is_link($root)) return null; $real = realpath($root); if ($real === false || !is_dir($real) || !is_writable($real)) return null; $directory = $real . DIRECTORY_SEPARATOR . self::DIRECTORY; clearstatcache(true, $directory); if (is_link($directory)) return null; if (!is_dir($directory) && !@mkdir($directory, 0700) && !is_dir($directory)) return null; clearstatcache(true, $directory); if (is_link($directory) || realpath($directory) !== $directory || !is_dir($directory)) return null; if (!@chmod($directory, 0700)) return null; $directoryStat = @stat($directory); if ($directoryStat === false || (PHP_OS_FAMILY !== 'Windows' && ($directoryStat['mode'] & 077) !== 0)) return null; $path = $directory . DIRECTORY_SEPARATOR . sprintf('%03d.cache', hexdec(substr($token, 0, 4)) % self::SLOTS); clearstatcache(true, $path); if (is_link($path) || (file_exists($path) && !is_file($path))) return null; $handle = @fopen($path, 'x+b'); if ($handle === false) $handle = @fopen($path, 'r+b'); if ($handle === false) return null; clearstatcache(true, $path); $opened = fstat($handle); $current = @lstat($path); if (is_link($path) || realpath($path) !== $path || $opened === false || $current === false || ($opened['mode'] & 0170000) !== 0100000 || ($current['mode'] & 0170000) !== 0100000 || $opened['dev'] !== $current['dev'] || $opened['ino'] !== $current['ino'] || $opened['nlink'] !== 1 || $current['nlink'] !== 1 || !@chmod($path, 0600)) { fclose($handle); return null; } return $handle; } private static function read($handle, string $token, bool $fresh = true): ?array { $size = (fstat($handle) ?: [])['size'] ?? 0; if ($size < 1 || $size > self::MAX_RECORD_BYTES || !rewind($handle)) return null; $raw = stream_get_contents($handle, self::MAX_RECORD_BYTES + 1); try { $data = is_string($raw) ? json_decode($raw, true, 8, JSON_THROW_ON_ERROR) : null; } catch (\Throwable $ignored) { return null; } if (!is_array($data) || ($data['v'] ?? null) !== self::VERSION || !is_string($data['token'] ?? null) || !hash_equals($token, $data['token']) || !self::validId($data['id'] ?? null) || !is_string($data['generation'] ?? null) || preg_match('/^[a-f0-9]{32}$/D', $data['generation']) !== 1 || !is_numeric($data['created'] ?? null) || !is_numeric($data['expires'] ?? null)) return null; $created = (float) $data['created']; $expires = (float) $data['expires']; $now = microtime(true); if (!is_finite($created) || !is_finite($expires) || $expires <= $created || $expires - $created > 120.001 || ($fresh && ($now < $created - 1 || $now >= $expires))) return null; return $data; } private static function write($handle, array $record): bool { $raw = json_encode($record, JSON_THROW_ON_ERROR); if (strlen($raw) > self::MAX_RECORD_BYTES || !self::clear($handle)) return false; if (@fwrite($handle, $raw) !== strlen($raw) || !@fflush($handle)) { self::clear($handle); return false; } return true; } private static function clear($handle): bool { return @rewind($handle) && @ftruncate($handle, 0) && @fflush($handle); } private static function validId($id): bool { return is_string($id) && preg_match('/^[a-zA-Z0-9_-]{1,128}$/D', $id) === 1; } private static function check(float $deadline, ?callable $abort): void { if ($abort !== null && $abort()) throw new \RuntimeException('AI_ASSISTANT_CANCELLED'); if (microtime(true) >= $deadline) throw new \RuntimeException('REQUIRED_FILES_TIMEOUT'); } }