database = $database; $this->files_table = $tables['code_files']; $this->symbols_table = $tables['code_symbols']; $this->scope = $scope; $this->signals = $signals; } public function enqueue( int $run_id, string $scope, string $path, string $item_type ): bool { $accepted = $this->scope->evaluate( $scope, $path, $item_type ); if ( $run_id < 1 || null === $accepted ) { return false; } $hash = $this->scope->reference( $scope, $accepted['path'] ); $extension = 'file' === $item_type ? strtolower( (string) pathinfo( $accepted['path'], PATHINFO_EXTENSION ) ) : ''; $masked = $this->scope->masked_path( $scope, $accepted['path'], $item_type ); $now = current_time( 'mysql', true ); $size_bytes = 'file' === $item_type ? max( 0, (int) filesize( $accepted['path'] ) ) : 0; // Runtime paths are plugin-owned queue data and are cleared atomically at // terminal state. The count subquery enforces the hard snapshot cap. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $result = $this->database->query( $this->database->prepare( "INSERT IGNORE INTO {$this->files_table} (run_id,file_hash,scope,item_type,extension,state,runtime_path,masked_path,size_bytes,signals_json,error_code,created_at,updated_at) SELECT %d,%s,%s,%s,%s,%s,%s,%s,%d,%s,NULL,%s,%s FROM DUAL WHERE (SELECT COUNT(*) FROM {$this->files_table} WHERE run_id = %d) < %d", $run_id, $hash, $scope, $item_type, $extension, self::STATE_QUEUED, $accepted['path'], $masked, $size_bytes, '{}', $now, $now, $run_id, self::MAX_ITEMS ) ); if ( false === $result ) { throw new StorageException( 'code_enqueue_failed' ); } return 1 === $result; } public function record_metadata( int $run_id, string $scope, string $path, string $signal_type ): bool { $accepted = $this->scope->evaluate( $scope, $path, is_dir( $path ) ? 'directory' : 'file' ); if ( $run_id < 1 || null === $accepted || 1 !== preg_match( '/^metadata\.[a-z][a-z0-9_]{1,47}$/', $signal_type ) ) { return false; } $signals = array( 'signal_types' => array( $signal_type ), 'occurrences' => array(), 'symbols' => array(), 'truncated' => false, 'syntax_state' => 'metadata', ); $this->signals->validate( $signals ); $json = wp_json_encode( $signals, JSON_UNESCAPED_SLASHES ); if ( false === $json ) { throw new StorageException( 'code_signals_encode_failed' ); } $hash = $this->scope->reference( $scope, $accepted['path'] . "\0" . $signal_type ); $now = current_time( 'mysql', true ); $masked = str_replace( '_', '-', $scope ) . '/metadata-' . substr( $hash, 0, 12 ); // The original path is never stored for metadata-only observations. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $result = $this->database->query( $this->database->prepare( "INSERT IGNORE INTO {$this->files_table} (run_id,file_hash,scope,item_type,extension,state,runtime_path,masked_path,size_bytes,signals_json,error_code,created_at,updated_at) SELECT %d,%s,%s,%s,%s,%s,NULL,%s,0,%s,NULL,%s,%s FROM DUAL WHERE (SELECT COUNT(*) FROM {$this->files_table} WHERE run_id = %d) < %d", $run_id, $hash, $scope, 'file', 'metadata', self::STATE_OBSERVED, $masked, $json, $now, $now, $run_id, self::MAX_ITEMS ) ); if ( false === $result ) { throw new StorageException( 'code_metadata_failed' ); } return 1 === $result; } public function record_skipped_reference( int $run_id, string $scope, string $path, string $error_code ): bool { if ( $run_id < 1 || 1 !== preg_match( '/^[a-z][a-z0-9_]{1,63}$/', $error_code ) ) { throw new InvalidArgumentException( 'invalid_code_skip' ); } $hash = $this->scope->reference( $scope, $path . "\0" . $error_code ); $extension = strtolower( (string) pathinfo( $path, PATHINFO_EXTENSION ) ); if ( ! in_array( $extension, array( 'php', 'inc', 'phtml', 'js', 'mjs', 'cjs', 'css' ), true ) ) { $extension = 'metadata'; } $now = current_time( 'mysql', true ); // Only a one-way reference is stored; the lexical source path is discarded. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $result = $this->database->query( $this->database->prepare( "INSERT IGNORE INTO {$this->files_table} (run_id,file_hash,scope,item_type,extension,state,runtime_path,masked_path,size_bytes,signals_json,error_code,created_at,updated_at) SELECT %d,%s,%s,%s,%s,%s,NULL,%s,0,%s,%s,%s,%s FROM DUAL WHERE (SELECT COUNT(*) FROM {$this->files_table} WHERE run_id = %d) < %d", $run_id, $hash, $scope, 'file', $extension, self::STATE_SKIPPED, str_replace( '_', '-', $scope ) . '/skipped-' . substr( $hash, 0, 12 ), '{}', $error_code, $now, $now, $run_id, self::MAX_ITEMS ) ); if ( false === $result ) { throw new StorageException( 'code_skip_record_failed' ); } return 1 === $result; } /** * Persist a recognized database snippet after it has been tokenized entirely * in memory. Neither the snippet ID nor source is retained. * * @param array $signals Source-free tokenizer signals. */ public function record_virtual_code( int $run_id, string $scope, string $reference, array $signals, int $size_bytes ): bool { if ( $run_id < 1 || 'code_snippets' !== $scope || $size_bytes < 0 || $size_bytes > 1048576 ) { throw new InvalidArgumentException( 'invalid_virtual_code' ); } $this->signals->validate( $signals ); $symbols = $signals['symbols']; $signals['symbols'] = array(); $json = wp_json_encode( $signals, JSON_UNESCAPED_SLASHES ); if ( false === $json || strlen( $json ) > self::MAX_SIGNAL_BYTES ) { throw new StorageException( 'code_signals_too_large' ); } $file_hash = $this->scope->reference( $scope, 'virtual:' . $reference ); $now = current_time( 'mysql', true ); // The provider reference is keyed before insertion; source and raw provider // identifiers never enter plugin storage. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $result = $this->database->query( $this->database->prepare( "INSERT IGNORE INTO {$this->files_table} (run_id,file_hash,scope,item_type,extension,state,runtime_path,masked_path,size_bytes,signals_json,error_code,created_at,updated_at) SELECT %d,%s,%s,%s,%s,%s,NULL,%s,%d,%s,NULL,%s,%s FROM DUAL WHERE (SELECT COUNT(*) FROM {$this->files_table} WHERE run_id = %d) < %d", $run_id, $file_hash, $scope, 'file', 'php', self::STATE_OBSERVED, 'code-snippets/snippet-' . substr( $file_hash, 0, 12 ) . '.php', $size_bytes, $json, $now, $now, $run_id, self::MAX_ITEMS ) ); if ( false === $result ) { throw new StorageException( 'virtual_code_insert_failed' ); } if ( 1 !== $result ) { return false; } $this->insert_symbols( $run_id, $file_hash, $symbols ); return true; } /** * Persist only the fact that a recognized virtual source exceeded a hard * limit. Provider identifiers and source are keyed before storage. */ public function record_virtual_skip( int $run_id, string $scope, string $reference, string $error_code, int $size_bytes ): bool { if ( $run_id < 1 || 'code_snippets' !== $scope || $size_bytes < 0 || 1 !== preg_match( '/^[a-z][a-z0-9_]{1,63}$/', $error_code ) ) { throw new InvalidArgumentException( 'invalid_virtual_code_skip' ); } $file_hash = $this->scope->reference( $scope, 'virtual:' . $reference ); $now = current_time( 'mysql', true ); // Source and the provider's primary key are never inserted. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $result = $this->database->query( $this->database->prepare( "INSERT IGNORE INTO {$this->files_table} (run_id,file_hash,scope,item_type,extension,state,runtime_path,masked_path,size_bytes,signals_json,error_code,created_at,updated_at) SELECT %d,%s,%s,%s,%s,%s,NULL,%s,%d,%s,%s,%s,%s FROM DUAL WHERE (SELECT COUNT(*) FROM {$this->files_table} WHERE run_id = %d) < %d", $run_id, $file_hash, $scope, 'file', 'php', self::STATE_SKIPPED, 'code-snippets/snippet-' . substr( $file_hash, 0, 12 ) . '.php', $size_bytes, '{}', $error_code, $now, $now, $run_id, self::MAX_ITEMS ) ); if ( false === $result ) { throw new StorageException( 'virtual_code_skip_failed' ); } return 1 === $result; } /** * Make a run-wide discovery limit visible to every source analyzer without * retaining an additional path or source value. */ public function mark_discovery_limitation( int $run_id, string $error_code ): bool { if ( $run_id < 1 || 1 !== preg_match( '/^[a-z][a-z0-9_]{1,63}$/', $error_code ) ) { throw new InvalidArgumentException( 'invalid_code_limitation' ); } // Table name is compiled by Schema; the bounded update touches one owned row. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $updated = $this->database->query( $this->database->prepare( "UPDATE {$this->files_table} SET error_code = %s,updated_at = %s WHERE run_id = %d AND item_type = %s ORDER BY id ASC LIMIT 1", $error_code, current_time( 'mysql', true ), $run_id, 'directory' ) ); if ( false === $updated ) { throw new StorageException( 'code_limitation_marker_failed' ); } return 1 === $updated; } /** * @return array{id:int,file_hash:string,scope:string,item_type:string,extension:string,path:string,size_bytes:int,directory_offset:int}|null */ public function next_queued( int $run_id ): ?array { if ( $run_id < 1 ) { throw new InvalidArgumentException( 'invalid_run_id' ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $row = $this->database->get_row( $this->database->prepare( "SELECT id,file_hash,scope,item_type,extension,runtime_path,size_bytes,signals_json FROM {$this->files_table} WHERE run_id = %d AND state = %s AND runtime_path IS NOT NULL ORDER BY id ASC LIMIT 1", $run_id, self::STATE_QUEUED ), ARRAY_A ); if ( ! is_array( $row ) ) { return null; } $accepted = $this->scope->evaluate( (string) $row['scope'], (string) $row['runtime_path'], (string) $row['item_type'] ); if ( null === $accepted || ! hash_equals( (string) $row['file_hash'], $this->scope->reference( $accepted['scope'], $accepted['path'] ) ) ) { throw new StorageException( 'invalid_stored_code_path' ); } return array( 'id' => (int) $row['id'], 'file_hash' => (string) $row['file_hash'], 'scope' => $accepted['scope'], 'item_type' => $accepted['type'], 'extension' => (string) $row['extension'], 'path' => $accepted['path'], 'size_bytes' => (int) $row['size_bytes'], 'directory_offset' => 'directory' === $accepted['type'] ? $this->directory_offset( (string) $row['signals_json'] ) : 0, ); } public function checkpoint_directory( int $run_id, int $id, string $file_hash, int $entry_offset ): bool { if ( $run_id < 1 || $id < 1 || $entry_offset < 1 || $entry_offset > self::MAX_ITEMS || 1 !== preg_match( '/^[a-f0-9]{64}$/', $file_hash ) ) { throw new InvalidArgumentException( 'invalid_code_directory_checkpoint' ); } $json = wp_json_encode( array( 'entry_offset' => $entry_offset ) ); if ( false === $json ) { throw new StorageException( 'code_directory_cursor_encode_failed' ); } // Raw path remains only because this directory is still queued. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $updated = $this->database->query( $this->database->prepare( "UPDATE {$this->files_table} SET signals_json = %s,updated_at = %s WHERE id = %d AND run_id = %d AND file_hash = %s AND state = %s", $json, current_time( 'mysql', true ), $id, $run_id, $file_hash, self::STATE_QUEUED ) ); return 1 === $updated; } /** * @param array $signals Source-free tokenizer signals. */ public function complete( int $run_id, int $id, string $file_hash, array $signals ): bool { if ( $run_id < 1 || $id < 1 || 1 !== preg_match( '/^[a-f0-9]{64}$/', $file_hash ) ) { throw new InvalidArgumentException( 'invalid_code_completion' ); } $this->signals->validate( $signals ); $symbols = $signals['symbols']; $signals['symbols'] = array(); $json = wp_json_encode( $signals, JSON_UNESCAPED_SLASHES ); if ( false === $json || strlen( $json ) > self::MAX_SIGNAL_BYTES ) { throw new StorageException( 'code_signals_too_large' ); } // Raw runtime path is cleared in the same compare-and-swap that commits // the source-free observation. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $updated = $this->database->query( $this->database->prepare( "UPDATE {$this->files_table} SET state = %s,runtime_path = NULL,signals_json = %s,error_code = NULL,updated_at = %s WHERE id = %d AND run_id = %d AND file_hash = %s AND state = %s", self::STATE_OBSERVED, $json, current_time( 'mysql', true ), $id, $run_id, $file_hash, self::STATE_QUEUED ) ); if ( 1 !== $updated ) { return false; } $this->insert_symbols( $run_id, $file_hash, $symbols ); return true; } public function finish_directory( int $run_id, int $id, string $file_hash, ?string $limitation_code = null ): bool { if ( null !== $limitation_code && 1 !== preg_match( '/^[a-z][a-z0-9_]{1,63}$/', $limitation_code ) ) { throw new InvalidArgumentException( 'invalid_code_limitation' ); } return $this->terminal_without_signals( $run_id, $id, $file_hash, self::STATE_OBSERVED, $limitation_code ); } public function skip( int $run_id, int $id, string $file_hash, string $error_code, bool $error = false ): bool { if ( 1 !== preg_match( '/^[a-z][a-z0-9_]{1,63}$/', $error_code ) ) { throw new InvalidArgumentException( 'invalid_code_error_code' ); } return $this->terminal_without_signals( $run_id, $id, $file_hash, $error ? self::STATE_ERROR : self::STATE_SKIPPED, $error_code ); } public function clear_pending( int $run_id, string $error_code ): int { if ( $run_id < 1 || 1 !== preg_match( '/^[a-z][a-z0-9_]{1,63}$/', $error_code ) ) { throw new InvalidArgumentException( 'invalid_code_clear' ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $result = $this->database->query( $this->database->prepare( "UPDATE {$this->files_table} SET state = %s,runtime_path = NULL,signals_json = %s,error_code = %s,updated_at = %s WHERE run_id = %d AND state = %s", self::STATE_SKIPPED, '{}', $error_code, current_time( 'mysql', true ), $run_id, self::STATE_QUEUED ) ); if ( false === $result ) { throw new StorageException( 'code_clear_failed' ); } return $result; } /** * @return array{queued:int,observed:int,skipped:int,error:int,total:int} */ public function counts( int $run_id ): array { $counts = array_fill_keys( array( self::STATE_QUEUED, self::STATE_OBSERVED, self::STATE_SKIPPED, self::STATE_ERROR ), 0 ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $rows = $this->database->get_results( $this->database->prepare( "SELECT state,COUNT(*) AS item_count FROM {$this->files_table} WHERE run_id = %d GROUP BY state", $run_id ), ARRAY_A ); foreach ( $rows as $row ) { if ( isset( $counts[ $row['state'] ] ) ) { $counts[ $row['state'] ] = (int) $row['item_count']; } } return array( 'queued' => $counts[ self::STATE_QUEUED ], 'observed' => $counts[ self::STATE_OBSERVED ], 'skipped' => $counts[ self::STATE_SKIPPED ], 'error' => $counts[ self::STATE_ERROR ], 'total' => array_sum( $counts ), ); } /** * Aggregate only bounded, source-free rows for one analyzer. * * @param list $extensions Relevant extensions, or metadata. * @param list $signal_types Relevant signal vocabulary. * @return array */ public function signal_summary( int $run_id, array $extensions, array $signal_types ): array { foreach ( array_merge( $extensions, $signal_types ) as $value ) { if ( ! is_string( $value ) || 1 !== preg_match( '/^[a-z][a-z0-9_.]{0,47}$/', $value ) ) { throw new InvalidArgumentException( 'invalid_code_summary_filter' ); } } $extension_placeholders = implode( ',', array_fill( 0, count( $extensions ), '%s' ) ); $base_args = array_merge( array( $run_id ), $extensions ); // Dynamic placeholders correspond one-for-one to validated extension values. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $counts = $this->database->get_row( $this->database->prepare( "SELECT COUNT(*) AS total, SUM(CASE WHEN state = 'observed' THEN 1 ELSE 0 END) AS observed, SUM(CASE WHEN state = 'skipped' THEN 1 ELSE 0 END) AS skipped, SUM(CASE WHEN state = 'error' THEN 1 ELSE 0 END) AS errors, SUM(CASE WHEN signals_json LIKE '%\"truncated\":true%' THEN 1 ELSE 0 END) AS truncated FROM {$this->files_table} WHERE run_id = %d AND item_type = 'file' AND extension IN ({$extension_placeholders})", ...$base_args ), ARRAY_A ); $signal_counts = array(); foreach ( $signal_types as $type ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $signal_counts[ $type ] = (int) $this->database->get_var( $this->database->prepare( "SELECT COUNT(*) FROM {$this->files_table} WHERE run_id = %d AND item_type = 'file' AND extension IN ({$extension_placeholders}) AND state = %s AND signals_json LIKE %s", ...array_merge( $base_args, array( self::STATE_OBSERVED, '%"' . $this->database->esc_like( $type ) . '"%' ) ) ) ); } $samples = array(); if ( array() !== $signal_types ) { $like_sql = implode( ' OR ', array_fill( 0, count( $signal_types ), 'signals_json LIKE %s' ) ); $like_args = array_map( fn ( string $type ): string => '%"' . $this->database->esc_like( $type ) . '"%', $signal_types ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $rows = $this->database->get_results( $this->database->prepare( "SELECT file_hash,scope,extension,masked_path,signals_json FROM {$this->files_table} WHERE run_id = %d AND item_type = 'file' AND extension IN ({$extension_placeholders}) AND state = %s AND ({$like_sql}) ORDER BY id ASC LIMIT 12", ...array_merge( $base_args, array( self::STATE_OBSERVED ), $like_args ) ), ARRAY_A ); foreach ( $rows as $row ) { $decoded = $this->decode_signals( (string) $row['signals_json'] ); $matching = array_values( array_filter( $decoded['occurrences'], static fn ( array $occurrence ): bool => in_array( $occurrence['type'], $signal_types, true ) ) ); $samples[] = array( 'file_hash' => (string) $row['file_hash'], 'scope' => (string) $row['scope'], 'extension' => (string) $row['extension'], 'masked_path' => (string) $row['masked_path'], 'occurrences' => array_slice( $matching, 0, 4 ), ); } } // Directory-level discovery limitations apply to every analyzer even though // directories are not counted as source-file coverage. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $discovery_partial = (int) $this->database->get_var( $this->database->prepare( "SELECT COUNT(*) FROM {$this->files_table} WHERE run_id = %d AND item_type = %s AND error_code IS NOT NULL", $run_id, 'directory' ) ); return array( 'total' => (int) ( $counts['total'] ?? 0 ), 'observed' => (int) ( $counts['observed'] ?? 0 ), 'skipped' => (int) ( $counts['skipped'] ?? 0 ), 'errors' => (int) ( $counts['errors'] ?? 0 ), 'truncated' => 0 < (int) ( $counts['truncated'] ?? 0 ) || 0 < $discovery_partial, 'signal_counts' => $signal_counts, 'samples' => $samples, ); } /** * @return array{total:int,observed:int,skipped:int,errors:int,truncated:bool,signal_counts:array,samples:list>,duplicate_count:int} */ public function symbol_summary( int $run_id ): array { $summary = $this->signal_summary( $run_id, array( 'php', 'inc', 'phtml' ), array() ); // Hashes cannot identify source symbols but can prove a repeated declaration. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $rows = $this->database->get_results( $this->database->prepare( "SELECT symbol_hash,symbol_type,COUNT(DISTINCT file_hash) AS file_count FROM {$this->symbols_table} WHERE run_id = %d GROUP BY symbol_hash,symbol_type HAVING COUNT(DISTINCT file_hash) > 1 ORDER BY file_count DESC LIMIT 12", $run_id ), ARRAY_A ); $summary['duplicate_count'] = count( $rows ); $summary['samples'] = array_map( static fn ( array $row ): array => array( 'symbol_hash' => (string) $row['symbol_hash'], 'symbol_type' => (string) $row['symbol_type'], 'file_count' => (int) $row['file_count'], ), $rows ); return $summary; } /** * @return array */ public function ownership_summary( int $run_id ): array { $types = array( 'metadata.git', 'metadata.composer_manifest', 'metadata.composer_lock', 'metadata.package_manifest', 'metadata.package_lock' ); $summary = $this->signal_summary( $run_id, array( 'metadata' ), $types ); // A component is an observed source scope, not an individual manifest file. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $scope_count = (int) $this->database->get_var( $this->database->prepare( "SELECT COUNT(DISTINCT scope) FROM {$this->files_table} WHERE run_id = %d AND item_type = %s", $run_id, 'file' ) ); $summary['total'] = $scope_count; $summary['observed'] = $scope_count; $summary['skipped'] = 0; $summary['errors'] = 0; return $summary; } private function terminal_without_signals( int $run_id, int $id, string $file_hash, string $state, ?string $error_code ): bool { if ( $run_id < 1 || $id < 1 || 1 !== preg_match( '/^[a-f0-9]{64}$/', $file_hash ) ) { throw new InvalidArgumentException( 'invalid_code_completion' ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $updated = $this->database->query( $this->database->prepare( "UPDATE {$this->files_table} SET state = %s,runtime_path = NULL,signals_json = %s,error_code = NULLIF(%s,''),updated_at = %s WHERE id = %d AND run_id = %d AND file_hash = %s AND state = %s", $state, '{}', $error_code, current_time( 'mysql', true ), $id, $run_id, $file_hash, self::STATE_QUEUED ) ); return 1 === $updated; } /** @param list $symbols Hashed symbol metadata. */ private function insert_symbols( int $run_id, string $file_hash, array $symbols ): void { $seen = array(); foreach ( $symbols as $symbol ) { $key = $symbol['symbol_hash'] . ':' . $symbol['kind']; if ( isset( $seen[ $key ] ) ) { continue; } $seen[ $key ] = true; // Only keyed hashes and numeric metadata are persisted. $result = $this->database->insert( $this->symbols_table, array( 'run_id' => $run_id, 'file_hash' => $file_hash, 'symbol_hash' => $symbol['symbol_hash'], 'symbol_type' => $symbol['kind'], 'line_number' => $symbol['line'], ), array( '%d', '%s', '%s', '%s', '%d' ) ); if ( false === $result ) { throw new StorageException( 'code_symbol_insert_failed' ); } } } /** @return array */ private function decode_signals( string $json ): array { if ( strlen( $json ) > self::MAX_SIGNAL_BYTES ) { throw new StorageException( 'invalid_code_signals' ); } try { $value = json_decode( $json, true, 32, JSON_THROW_ON_ERROR ); } catch ( JsonException $exception ) { // Previous exception remains local and is never rendered. // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped throw new StorageException( 'invalid_code_signals', 0, $exception ); } if ( ! is_array( $value ) ) { throw new StorageException( 'invalid_code_signals' ); } $value['symbols'] = array(); $this->signals->validate( $value ); return $value; } private function directory_offset( string $json ): int { if ( '{}' === $json ) { return 0; } try { $value = json_decode( $json, true, 4, JSON_THROW_ON_ERROR ); } catch ( JsonException $exception ) { // Previous exception remains local and is never rendered. // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped throw new StorageException( 'invalid_code_directory_cursor', 0, $exception ); } if ( ! is_array( $value ) || array( 'entry_offset' ) !== array_keys( $value ) || ! is_int( $value['entry_offset'] ) || $value['entry_offset'] < 1 || $value['entry_offset'] > self::MAX_ITEMS ) { throw new StorageException( 'invalid_code_directory_cursor' ); } return $value['entry_offset']; } }