'ogg', 'ID3' => 'mp3', ]; protected static function filePattern(): string { return '[a-f0-9]{64}\.(mp3|ogg|wav|m4a)'; } /** * Stores the raw bytes of a sound and returns its record. * * The format is read from the file's own signature, never from anything the client says – * browsers report a dropped `.m4a` as `audio/mp4`, `audio/x-m4a` or nothing at all, and an * `.ogg` as either `audio/ogg` or `video/ogg`. * * `$originalName` is kept beside the file as a caption (see OriginalName): the hash says * nothing, and there is no tag in here that is worth trusting for a label. * * @return array{file:string,name:string,bytes:int} * @throws InvalidSoundException when the bytes are not a supported sound */ public function store(string $bytes, string $originalName, string $folder): array { $extension = self::extensionOf($bytes); if ($extension === '') { throw new InvalidSoundException('Not a sound file – use MP3, OGG, WAV or M4A'); } return $this->keep($bytes, $extension, $originalName, $folder); } /** The format these bytes are, `''` when they are none we take. */ private static function extensionOf(string $bytes): string { // The shortest thing that can still carry a header of any of the four formats. if (strlen($bytes) < 12) { return ''; } foreach (self::SIGNATURES as $magic => $extension) { if (str_starts_with($bytes, $magic)) { return $extension; } } // Both are RIFF/ISO container formats, so the type sits past the size field, not at byte 0. if (str_starts_with($bytes, 'RIFF') && substr($bytes, 8, 4) === 'WAVE') { return 'wav'; } if (substr($bytes, 4, 4) === 'ftyp') { return 'm4a'; } return self::mp3Extension($bytes); } /** * A tagless MP3: a frame header is eleven set bits (`0xFF 0xEx`/`0xFx`), and the two bits below * them name the MPEG version, of which `01` is reserved – demanding that much keeps a random * binary that happens to start with `0xFF` out. */ private static function mp3Extension(string $bytes): string { $first = ord($bytes[0]); $second = ord($bytes[1]); $version = ($second >> 3) & 0b11; return $first === 0xFF && ($second & 0xE0) === 0xE0 && $version !== 0b01 ? 'mp3' : ''; } } /** Thrown by SoundStore::store when the uploaded bytes are not a usable sound. */ final class InvalidSoundException extends RuntimeException { }