$manifest */ public static function render(array $manifest, int $budgetBytes): string { $header = self::headerLine($manifest); $anythingEncoded = false; foreach (array($manifest, self::withoutIdLists($manifest), self::minimal($manifest)) as $candidate) { $line = self::encodeOrEmpty(array(ABJ_404_Solution_DiagnosticCollectionManifest::RECORD_KEY => $candidate)); if ($line === '') { continue; } $anythingEncoded = true; if (strlen($header) + strlen($line) <= $budgetBytes) { return $header . $line; } } // Two different reasons reach here and they are not the same finding. // Everything encoded but nothing fit means the budget was too small -- // the site is fine and the payload was squeezed. Nothing encoding at // all means the manifest itself held bytes json_encode() rejected, // which is a defect worth chasing. Reporting both as `encoding_failed` // sent a reader after the wrong one, in the very record whose job is // to explain why the rest of the payload is thin. return $header . self::encodeOrEmpty(array(ABJ_404_Solution_DiagnosticCollectionManifest::RECORD_KEY => array( 'reduced' => $anythingEncoded ? 'all_forms_over_budget' : 'encoding_failed', 'outcome' => self::stringOf($manifest['outcome'] ?? ABJ_404_Solution_DiagnosticCollectionManifest::OUTCOME_EMPTY), ))); } /** * The scannable one-line version, so the first thing a reader sees is what * was looked for and how much of it was there. * * @param array $manifest */ private static function headerLine(array $manifest): string { $channels = self::arrayOf($manifest['channels'] ?? null); $checked = 0; $found = 0; $lines = 0; foreach ($channels as $channel) { $described = self::arrayOf($channel); $checked += self::intOf($described['candidates_checked'] ?? 0); $found += self::intOf($described['candidates_found'] ?? 0); $lines += self::intOf($described['collected_lines'] ?? 0); } return 'Diagnostic collection manifest -- ' . count($channels) . ' channel(s), ' . $found . ' of ' . $checked . ' candidate files present, ' . $lines . ' lines collected; ' . self::attemptClause(self::arrayOf($manifest['client_expected_attempts'] ?? null)) . " (JSONL):\n"; } /** @param array $expected */ private static function attemptClause(array $expected): string { $status = self::stringOf($expected['status'] ?? 'absent'); if ($status === 'parsed') { return 'browser expected ' . self::intOf($expected['expected'] ?? 0) . ' attempt id(s), ' . self::intOf($expected['found'] ?? 0) . ' present'; } if ($status === 'unparseable') { return 'the browser attempt buffer could not be parsed'; } return 'no browser attempt ids were sent'; } /** * @param array $manifest * @return array */ private static function withoutIdLists(array $manifest): array { $manifest['reduced'] = 'attempt_id_lists_and_file_stats_dropped_for_budget'; $expected = self::arrayOf($manifest['client_expected_attempts'] ?? null); $expected['found_ids'] = array(); $expected['missing_ids'] = array(); $manifest['client_expected_attempts'] = $expected; $channels = array(); foreach (self::arrayOf($manifest['channels'] ?? null) as $channel) { $described = self::arrayOf($channel); unset($described['files']); $selection = self::arrayOf($described['file_selection'] ?? null); $selection['dropped_file_names'] = array(); $selection['dropped_request_ids'] = array(); $described['file_selection'] = $selection; $channels[] = $described; } $manifest['channels'] = $channels; return $manifest; } /** * The smallest manifest that is still worth having: who collected, how many * files each channel checked and found, and the outcome. * * @param array $manifest * @return array */ private static function minimal(array $manifest): array { $channels = array(); foreach (self::arrayOf($manifest['channels'] ?? null) as $channel) { $described = self::arrayOf($channel); $selection = self::arrayOf($described['file_selection'] ?? null); $minimalSelection = array( 'policy' => self::stringOf($selection['policy'] ?? ''), ); foreach (array( 'existing_files', 'selected_files', 'known_failure_files', 'server_failure_files', 'classification_issue_files', 'pinned_files', 'classification_issues_omitted', 'dropped_files', 'dropped_file_names_omitted', 'dropped_request_ids_omitted', ) as $field) { $minimalSelection[$field] = self::intOf($selection[$field] ?? 0); } $channels[] = array( 'channel' => self::stringOf($described['channel'] ?? 'unknown'), 'directory_usable' => !empty($described['directory_usable']), 'candidates_checked' => self::intOf($described['candidates_checked'] ?? 0), 'candidates_found' => self::intOf($described['candidates_found'] ?? 0), 'collected_bytes' => self::intOf($described['collected_bytes'] ?? 0), 'file_selection' => $minimalSelection, ); } return array( 'reduced' => 'channel_detail_dropped_for_budget', 'collector' => self::arrayOf($manifest['collector'] ?? null), 'channels' => $channels, 'required_evidence_records' => self::arrayOf($manifest['required_evidence_records'] ?? null), 'outcome' => self::stringOf($manifest['outcome'] ?? ABJ_404_Solution_DiagnosticCollectionManifest::OUTCOME_EMPTY), ); } /** @param array $value */ public static function encodeOrEmpty(array $value): string { $json = json_encode($value, JSON_UNESCAPED_SLASHES); return is_string($json) ? $json : ''; } /** * Typed reads of the assembled record. The manifest is deliberately a * loose map (its shape is the wire format, not a PHP contract), so the * places that summarize it read through these rather than casting mixed. * * @param mixed $value */ private static function intOf($value): int { return ABJ_404_Solution_ExactInteger::readOr($value, 0, 0); } /** @param mixed $value */ private static function stringOf($value): string { return is_scalar($value) ? (string)$value : ''; } /** * @param mixed $value * @return array */ private static function arrayOf($value): array { return is_array($value) ? $value : array(); } }