Files
speech2text/app/Rules/ValidAudioMagicBytes.php
2026-06-02 17:35:45 +08:00

78 lines
2.4 KiB
PHP

<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Http\UploadedFile;
class ValidAudioMagicBytes implements ValidationRule
{
// Signatures checked at byte offset 0
private const SIGNATURES = [
[0xFF, 0xFB], // MP3 MPEG-1 Layer 3
[0xFF, 0xF3], // MP3 MPEG-2 Layer 3
[0xFF, 0xF2], // MP3 MPEG-2.5 Layer 3
[0xFF, 0xF1], // AAC ADTS MPEG-4
[0xFF, 0xF9], // AAC ADTS MPEG-2
[0x49, 0x44, 0x33], // MP3 with ID3v2 tag
[0x52, 0x49, 0x46, 0x46], // WAV (RIFF header)
[0x4F, 0x67, 0x67, 0x53], // OGG (OggS)
[0x66, 0x4C, 0x61, 0x43], // FLAC (fLaC)
[0x1A, 0x45, 0xDF, 0xA3], // WebM / MKV
];
// MP4/M4A: ISO Base Media file — 'ftyp' box starts at byte offset 4
private const MP4_SIGNATURE = [0x66, 0x74, 0x79, 0x70]; // 'ftyp'
private const MP4_OFFSET = 4;
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (! ($value instanceof UploadedFile) || ! $value->isValid()) {
$fail('Fail audio tidak sah.');
return;
}
$handle = @fopen($value->getRealPath(), 'rb');
if ($handle === false) {
$fail('Fail audio tidak dapat dibaca.');
return;
}
$header = fread($handle, 12);
fclose($handle);
if (strlen($header) < 4) {
$fail('Fail audio terlalu kecil atau rosak.');
return;
}
$bytes = array_values(unpack('C*', $header));
foreach (self::SIGNATURES as $sig) {
if ($this->matchesAt($bytes, $sig, 0)) {
return;
}
}
// Check MP4/M4A: need at least 8 bytes
if (count($bytes) >= self::MP4_OFFSET + count(self::MP4_SIGNATURE)) {
if ($this->matchesAt($bytes, self::MP4_SIGNATURE, self::MP4_OFFSET)) {
return;
}
}
$fail('Format fail audio tidak sah. Kandungan fail tidak menepati format audio yang dibenarkan.');
}
private function matchesAt(array $bytes, array $signature, int $offset): bool
{
foreach ($signature as $i => $expected) {
if (($bytes[$offset + $i] ?? -1) !== $expected) {
return false;
}
}
return true;
}
}