diff --git a/Kernel/CMakeLists.txt b/Kernel/CMakeLists.txt index aaec2fd6d6..ea58982611 100644 --- a/Kernel/CMakeLists.txt +++ b/Kernel/CMakeLists.txt @@ -110,6 +110,7 @@ set(KERNEL_SOURCES FileSystem/Inode.cpp FileSystem/InodeFile.cpp FileSystem/InodeWatcher.cpp + FileSystem/ISO9660FileSystem.cpp FileSystem/Mount.cpp FileSystem/Plan9FileSystem.cpp FileSystem/ProcFS.cpp diff --git a/Kernel/Debug.h.in b/Kernel/Debug.h.in index c9b8b6a368..aa2b30d043 100644 --- a/Kernel/Debug.h.in +++ b/Kernel/Debug.h.in @@ -126,6 +126,14 @@ #cmakedefine01 IO_DEBUG #endif +#ifndef ISO9660_DEBUG +#cmakedefine01 ISO9660_DEBUG +#endif + +#ifndef ISO9660_VERY_DEBUG +#cmakedefine01 ISO9660_VERY_DEBUG +#endif + #ifndef IPV4_DEBUG #cmakedefine01 IPV4_DEBUG #endif diff --git a/Kernel/FileSystem/ISO9660FileSystem.cpp b/Kernel/FileSystem/ISO9660FileSystem.cpp new file mode 100644 index 0000000000..41e1e47600 --- /dev/null +++ b/Kernel/FileSystem/ISO9660FileSystem.cpp @@ -0,0 +1,735 @@ +/* + * Copyright (c) 2021, sin-ack + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "ISO9660FileSystem.h" +#include "Kernel/FileSystem/BlockBasedFileSystem.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Kernel { + +// NOTE: According to the spec, logical blocks 0 to 15 are system use. +constexpr u32 first_data_area_block = 16; +constexpr u32 logical_sector_size = 2048; +constexpr u32 max_cached_directory_entries = 128; + +struct DirectoryState { + RefPtr entry; + u32 offset { 0 }; +}; + +class ISO9660DirectoryIterator { +public: + ISO9660DirectoryIterator(ISO9660FS& fs, ISO::DirectoryRecordHeader const& header) + : m_fs(fs) + , m_current_header(&header) + { + // FIXME: Panic or alternative method? + (void)read_directory_contents(); + get_header(); + } + + ISO::DirectoryRecordHeader const* operator*() { return m_current_header; } + + // Recurses into subdirectories. May fail. + KResultOr next() + { + if (done()) + return false; + dbgln_if(ISO9660_VERY_DEBUG, "next(): Called"); + + if (has_flag(m_current_header->file_flags, ISO::FileFlags::Directory)) { + dbgln_if(ISO9660_VERY_DEBUG, "next(): Recursing"); + { + bool result = m_directory_stack.try_append(move(m_current_directory)); + if (!result) { + return ENOMEM; + } + } + + dbgln_if(ISO9660_VERY_DEBUG, "next(): Pushed into directory stack"); + + { + auto result = read_directory_contents(); + if (result.is_error()) { + return result; + } + } + + dbgln_if(ISO9660_VERY_DEBUG, "next(): Read directory contents"); + + m_current_directory.offset = 0; + get_header(); + if (m_current_header->length == 0) { + // We have found an empty directory, let's continue with the + // next one. + if (!go_up()) + return false; + } else { + // We cannot skip here, as this is the first record in this + // extent. + return true; + } + } + + return skip(); + } + + // Skips to the directory in the list, returns whether there was a next one. + // No allocation here, cannot fail. + bool skip() + { + VERIFY(m_current_directory.entry); + + if (m_current_directory.offset >= m_current_directory.entry->length) { + dbgln_if(ISO9660_VERY_DEBUG, "skip(): Was at last item already"); + return false; + } + + m_current_directory.offset += m_current_header->length; + get_header(); + if (m_current_header->length == 0) { + // According to ECMA 119, if a logical block contains directory + // records, then the leftover bytes in the logical block are + // all zeros. So if our directory header has a length of 0, + // we're probably looking at padding. + // + // Of course, this doesn't mean we're done; it only means that there + // are no more directory entries in *this* logical block. If we + // have at least one more logical block of data length to go, we + // need to snap to the next logical block, because directory records + // cannot span multiple logical blocks. + u32 remaining_bytes = m_current_directory.entry->length - m_current_directory.offset; + if (remaining_bytes > m_fs.logical_block_size()) { + m_current_directory.offset += remaining_bytes % m_fs.logical_block_size(); + get_header(); + + dbgln_if(ISO9660_VERY_DEBUG, "skip(): Snapped to next logical block (succeeded)"); + return true; + } + + dbgln_if(ISO9660_VERY_DEBUG, "skip(): Was at the last logical block, at padding now (offset {}, data length {})", m_current_directory.entry->length, m_current_directory.offset); + return false; + } + + dbgln_if(ISO9660_VERY_DEBUG, "skip(): Skipped to next item"); + return true; + } + + bool go_up() + { + if (m_directory_stack.is_empty()) { + dbgln_if(ISO9660_VERY_DEBUG, "go_up(): Empty directory stack"); + return false; + } + + m_current_directory = m_directory_stack.take_last(); + get_header(); + + dbgln_if(ISO9660_VERY_DEBUG, "go_up(): Went up a directory"); + return true; + } + + bool done() const + { + VERIFY(m_current_directory.entry); + auto result = m_directory_stack.is_empty() && m_current_directory.offset >= m_current_directory.entry->length; + dbgln_if(ISO9660_VERY_DEBUG, "done(): {}", result); + return result; + } + +private: + KResult read_directory_contents() + { + auto result = m_fs.directory_entry_for_record({}, m_current_header); + if (result.is_error()) { + return result.error(); + } + + m_current_directory.entry = result.value(); + return KSuccess; + } + + void get_header() + { + VERIFY(m_current_directory.entry); + if (!m_current_directory.entry->blocks) + return; + + m_current_header = reinterpret_cast(m_current_directory.entry->blocks->data() + m_current_directory.offset); + } + + ISO9660FS& m_fs; + + DirectoryState m_current_directory; + ISO::DirectoryRecordHeader const* m_current_header { nullptr }; + + Vector m_directory_stack; +}; + +KResultOr> ISO9660FS::try_create(FileDescription& description) +{ + auto result = adopt_ref_if_nonnull(new (nothrow) ISO9660FS(description)); + if (!result) { + return ENOMEM; + } + return result.release_nonnull(); +} + +ISO9660FS::ISO9660FS(FileDescription& description) + : BlockBasedFileSystem(description) +{ + set_block_size(logical_sector_size); + m_logical_block_size = logical_sector_size; +} + +ISO9660FS::~ISO9660FS() +{ +} + +bool ISO9660FS::initialize() +{ + if (!BlockBasedFileSystem::initialize()) + return false; + + // FIXME: Fix the FileSystem::initialize contract to be able to return a + // KResult. + if (parse_volume_set().is_error()) + return false; + if (create_root_inode().is_error()) + return false; + return true; +} + +Inode& ISO9660FS::root_inode() +{ + VERIFY(!m_root_inode.is_null()); + return *m_root_inode; +} + +unsigned ISO9660FS::total_block_count() const +{ + return LittleEndian { m_primary_volume->volume_space_size.little }; +} + +unsigned ISO9660FS::total_inode_count() const +{ + if (!m_cached_inode_count) { + auto result = calculate_inode_count(); + if (result.is_error()) { + // FIXME: This should be able to return a KResult. + return 0; + } + } + + return m_cached_inode_count; +} + +u8 ISO9660FS::internal_file_type_to_directory_entry_type(const DirectoryEntryView& entry) const +{ + if (has_flag(static_cast(entry.file_type), ISO::FileFlags::Directory)) { + return DT_DIR; + } + + return DT_REG; +} + +KResult ISO9660FS::parse_volume_set() +{ + VERIFY(!m_primary_volume); + + auto block = KBuffer::try_create_with_size(m_logical_block_size, Memory::Region::Access::Read | Memory::Region::Access::Write, "ISO9660FS: Temporary volume descriptor storage"); + if (!block) { + return ENOMEM; + } + + auto block_buffer = UserOrKernelBuffer::for_kernel_buffer(block->data()); + + auto current_block_index = first_data_area_block; + while (true) { + bool result = raw_read(BlockIndex { current_block_index }, block_buffer); + if (!result) { + dbgln_if(ISO9660_DEBUG, "Failed to read volume descriptor from ISO file"); + return EIO; + } + + auto header = reinterpret_cast(block->data()); + if (StringView { header->identifier, 5 } != "CD001") { + dbgln_if(ISO9660_DEBUG, "Header magic at volume descriptor {} is not valid", current_block_index - first_data_area_block); + return EIO; + } + + switch (header->type) { + case ISO::VolumeDescriptorType::PrimaryVolumeDescriptor: { + auto primary_volume = reinterpret_cast(header); + m_primary_volume = adopt_own_if_nonnull(new ISO::PrimaryVolumeDescriptor(*primary_volume)); + break; + } + case ISO::VolumeDescriptorType::BootRecord: + case ISO::VolumeDescriptorType::SupplementaryOrEnhancedVolumeDescriptor: + case ISO::VolumeDescriptorType::VolumePartitionDescriptor: { + break; + } + case ISO::VolumeDescriptorType::VolumeDescriptorSetTerminator: { + goto all_headers_read; + } + default: + dbgln_if(ISO9660_DEBUG, "Unexpected volume descriptor type {} in volume set", static_cast(header->type)); + return EIO; + } + + current_block_index++; + } + +all_headers_read: + if (!m_primary_volume) { + dbgln_if(ISO9660_DEBUG, "Could not find primary volume"); + return EIO; + } + + m_logical_block_size = LittleEndian { m_primary_volume->logical_block_size.little }; + return KSuccess; +} + +KResult ISO9660FS::create_root_inode() +{ + if (!m_primary_volume) { + dbgln_if(ISO9660_DEBUG, "Primary volume doesn't exist, can't create root inode"); + return EIO; + } + + auto maybe_inode = ISO9660Inode::try_create_from_directory_record(*this, m_primary_volume->root_directory_record_header, {}); + if (maybe_inode.is_error()) { + return ENOMEM; + } + + m_root_inode = maybe_inode.release_value(); + return KSuccess; +} + +KResult ISO9660FS::calculate_inode_count() const +{ + if (!m_primary_volume) { + dbgln_if(ISO9660_DEBUG, "Primary volume doesn't exist, can't calculate inode count"); + return EIO; + } + + size_t inode_count = 1; + + auto traversal_result = visit_directory_record(m_primary_volume->root_directory_record_header, [&](ISO::DirectoryRecordHeader const* header) { + if (header == nullptr) { + return RecursionDecision::Continue; + } + + inode_count += 1; + + if (has_flag(header->file_flags, ISO::FileFlags::Directory)) { + if (header->file_identifier_length == 1) { + auto file_identifier = reinterpret_cast(header + 1); + if (file_identifier[0] == '\0' || file_identifier[0] == '\1') { + return RecursionDecision::Continue; + } + } + + return RecursionDecision::Recurse; + } + + return RecursionDecision::Continue; + }); + + if (traversal_result.is_error()) { + dbgln_if(ISO9660_DEBUG, "Failed to traverse for caching inode count!"); + return traversal_result; + } + + m_cached_inode_count = inode_count; + return KSuccess; +} + +KResult ISO9660FS::visit_directory_record(ISO::DirectoryRecordHeader const& record, Function(ISO::DirectoryRecordHeader const*)> const& visitor) const +{ + if (!has_flag(record.file_flags, ISO::FileFlags::Directory)) { + return KSuccess; + } + + ISO9660DirectoryIterator iterator { const_cast(*this), record }; + + while (!iterator.done()) { + auto maybe_result = visitor(*iterator); + if (maybe_result.is_error()) { + return maybe_result.error(); + } + + switch (maybe_result.value()) { + case RecursionDecision::Recurse: { + auto maybe_has_moved = iterator.next(); + if (maybe_has_moved.is_error()) { + return maybe_has_moved.error(); + } + + if (!maybe_has_moved.value()) { + // If next() hasn't moved then we have read through all the + // directories, and can exit. + return KSuccess; + } + + continue; + } + case RecursionDecision::Continue: { + while (!iterator.done()) { + if (iterator.skip()) + break; + if (!iterator.go_up()) + return KSuccess; + } + + continue; + } + case RecursionDecision::Break: + return KSuccess; + } + } + + return KSuccess; +} + +KResultOr> ISO9660FS::directory_entry_for_record(Badge, ISO::DirectoryRecordHeader const* record) +{ + u32 extent_location = LittleEndian { record->extent_location.little }; + u32 data_length = LittleEndian { record->data_length.little }; + + auto key = calculate_directory_entry_cache_key(*record); + auto it = m_directory_entry_cache.find(key); + if (it != m_directory_entry_cache.end()) { + dbgln_if(ISO9660_DEBUG, "Cache hit for dirent @ {}", extent_location); + return it->value; + } + dbgln_if(ISO9660_DEBUG, "Cache miss for dirent @ {} :^(", extent_location); + + if (m_directory_entry_cache.size() == max_cached_directory_entries) { + // FIXME: A smarter algorithm would probably be nicer. + m_directory_entry_cache.remove(m_directory_entry_cache.begin()); + } + + if (!(data_length % logical_block_size() == 0)) { + dbgln_if(ISO9660_DEBUG, "Found a directory with non-logical block size aligned data length!"); + return EIO; + } + + auto blocks = KBuffer::try_create_with_size(data_length, Memory::Region::Access::Read | Memory::Region::Access::Write, "ISO9660FS: Directory traversal buffer"); + if (!blocks) { + return ENOMEM; + } + + auto blocks_buffer = UserOrKernelBuffer::for_kernel_buffer(blocks->data()); + auto did_read = raw_read_blocks(BlockBasedFileSystem::BlockIndex { extent_location }, data_length / logical_block_size(), blocks_buffer); + if (!did_read) { + return EIO; + } + + auto maybe_entry = DirectoryEntry::try_create(extent_location, data_length, move(blocks)); + if (maybe_entry.is_error()) { + return maybe_entry.error(); + } + m_directory_entry_cache.set(key, maybe_entry.value()); + + dbgln_if(ISO9660_DEBUG, "Cached dirent @ {}", extent_location); + return maybe_entry.release_value(); +} + +u32 ISO9660FS::calculate_directory_entry_cache_key(ISO::DirectoryRecordHeader const& record) +{ + return LittleEndian { record.extent_location.little }; +} + +KResultOr ISO9660Inode::read_bytes(off_t offset, size_t size, UserOrKernelBuffer& buffer, FileDescription*) const +{ + MutexLocker inode_locker(m_inode_lock); + + auto& file_system = const_cast(static_cast(fs())); + u32 data_length = LittleEndian { m_record.data_length.little }; + u32 extent_location = LittleEndian { m_record.extent_location.little }; + + if (static_cast(offset) >= data_length) + return 0; + + auto block = KBuffer::try_create_with_size(file_system.m_logical_block_size); + if (!block) { + return ENOMEM; + } + auto block_buffer = UserOrKernelBuffer::for_kernel_buffer(block->data()); + + size_t total_bytes = min(size, data_length - offset); + size_t nread = 0; + size_t blocks_already_read = offset / file_system.m_logical_block_size; + size_t initial_offset = offset % file_system.m_logical_block_size; + + auto current_block_index = BlockBasedFileSystem::BlockIndex { extent_location + blocks_already_read }; + while (nread != total_bytes) { + size_t bytes_to_read = min(total_bytes - nread, file_system.logical_block_size() - initial_offset); + auto buffer_offset = buffer.offset(nread); + dbgln_if(ISO9660_VERY_DEBUG, "ISO9660Inode::read_bytes: Reading {} bytes into buffer offset {}/{}, logical block index: {}", bytes_to_read, nread, total_bytes, current_block_index.value()); + + if (bool result = file_system.raw_read(current_block_index, block_buffer); !result) { + return EIO; + } + + bool result = buffer_offset.write(block->data() + initial_offset, bytes_to_read); + if (!result) { + return EFAULT; + } + + nread += bytes_to_read; + initial_offset = 0; + current_block_index = BlockBasedFileSystem::BlockIndex { current_block_index.value() + 1 }; + } + + return nread; +} + +InodeMetadata ISO9660Inode::metadata() const +{ + return m_metadata; +} + +KResult ISO9660Inode::traverse_as_directory(Function visitor) const +{ + auto& file_system = static_cast(fs()); + Array file_identifier_buffer; + + auto traversal_result = file_system.visit_directory_record(m_record, [&](ISO::DirectoryRecordHeader const* record) { + StringView filename = get_normalized_filename(*record, file_identifier_buffer); + dbgln_if(ISO9660_VERY_DEBUG, "traverse_as_directory(): Found {}", filename); + + InodeIdentifier id { fsid(), get_inode_index(*record, filename) }; + auto entry = FileSystem::DirectoryEntryView(filename, id, static_cast(record->file_flags)); + + if (!visitor(entry)) { + return RecursionDecision::Break; + } + + return RecursionDecision::Continue; + }); + + if (traversal_result.is_error()) + return traversal_result; + + return KSuccess; +} + +RefPtr ISO9660Inode::lookup(StringView name) +{ + auto& file_system = static_cast(fs()); + RefPtr inode; + Array file_identifier_buffer; + + auto traversal_result = file_system.visit_directory_record(m_record, [&](ISO::DirectoryRecordHeader const* record) { + StringView filename = get_normalized_filename(*record, file_identifier_buffer); + + if (filename == name) { + auto maybe_inode = ISO9660Inode::try_create_from_directory_record(const_cast(file_system), *record, filename); + if (maybe_inode.is_error()) { + // FIXME: The Inode API does not handle allocation failures very + // well... we can't return a KResultOr from here. It + // would be nice if we could return a KResult(Or) from + // any place where allocation may happen. + dbgln("Could not allocate inode for lookup!"); + } else { + inode = maybe_inode.release_value(); + } + return RecursionDecision::Break; + } + + return RecursionDecision::Continue; + }); + + if (traversal_result.is_error()) { + return {}; + } + + return inode; +} + +void ISO9660Inode::flush_metadata() +{ +} + +KResultOr ISO9660Inode::write_bytes(off_t, size_t, const UserOrKernelBuffer&, FileDescription*) +{ + return EROFS; +} + +KResultOr> ISO9660Inode::create_child(StringView, mode_t, dev_t, uid_t, gid_t) +{ + return EROFS; +} + +KResult ISO9660Inode::add_child(Inode&, const StringView&, mode_t) +{ + return EROFS; +} + +KResult ISO9660Inode::remove_child(const StringView&) +{ + return EROFS; +} + +KResult ISO9660Inode::chmod(mode_t) +{ + return EROFS; +} + +KResult ISO9660Inode::chown(uid_t, gid_t) +{ + return EROFS; +} + +KResult ISO9660Inode::truncate(u64) +{ + return EROFS; +} + +KResult ISO9660Inode::set_atime(time_t) +{ + return EROFS; +} + +KResult ISO9660Inode::set_ctime(time_t) +{ + return EROFS; +} + +KResult ISO9660Inode::set_mtime(time_t) +{ + return EROFS; +} + +void ISO9660Inode::one_ref_left() +{ +} + +ISO9660Inode::ISO9660Inode(ISO9660FS& fs, ISO::DirectoryRecordHeader const& record, StringView const& name) + : Inode(fs, get_inode_index(record, name)) + , m_record(record) +{ + dbgln_if(ISO9660_VERY_DEBUG, "Creating inode #{}", index()); + create_metadata(); +} + +ISO9660Inode::~ISO9660Inode() +{ +} + +KResultOr> ISO9660Inode::try_create_from_directory_record(ISO9660FS& fs, ISO::DirectoryRecordHeader const& record, StringView const& name) +{ + auto result = adopt_ref_if_nonnull(new (nothrow) ISO9660Inode(fs, record, name)); + if (!result) { + return ENOMEM; + } + return result.release_nonnull(); +} + +void ISO9660Inode::create_metadata() +{ + u32 data_length = LittleEndian { m_record.data_length.little }; + bool is_directory = has_flag(m_record.file_flags, ISO::FileFlags::Directory); + time_t recorded_at = parse_numerical_date_time(m_record.recording_date_and_time); + + m_metadata = { + .inode = identifier(), + .size = data_length, + .mode = static_cast((is_directory ? S_IFDIR : S_IFREG) | (is_directory ? 0555 : 0444)), + .uid = 0, + .gid = 0, + .link_count = 1, + .atime = recorded_at, + .ctime = recorded_at, + .mtime = recorded_at, + .dtime = 0, + .block_count = 0, + .block_size = 0, + .major_device = 0, + .minor_device = 0, + }; +} + +time_t ISO9660Inode::parse_numerical_date_time(ISO::NumericalDateAndTime const& date) +{ + i32 year_offset = date.years_since_1900 - 70; + + return (year_offset * 60 * 60 * 24 * 30 * 12) + + (date.month * 60 * 60 * 24 * 30) + + (date.day * 60 * 60 * 24) + + (date.hour * 60 * 60) + + (date.minute * 60) + + date.second; +} + +StringView ISO9660Inode::get_normalized_filename(ISO::DirectoryRecordHeader const& record, Bytes buffer) +{ + auto file_identifier = reinterpret_cast(&record + 1); + auto filename = StringView { file_identifier, record.file_identifier_length }; + + if (filename.length() == 1) { + if (filename[0] == '\0') { + filename = "."sv; + } + + if (filename[0] == '\1') { + filename = ".."sv; + } + } + + if (!has_flag(record.file_flags, ISO::FileFlags::Directory)) { + // FIXME: We currently strip the file version from the filename, + // but that may be used later down the line if the file actually + // has multiple versions on the disk. + Optional semicolon = filename.find(';'); + if (semicolon.has_value()) { + filename = filename.substring_view(0, semicolon.value()); + } + + if (filename[filename.length() - 1] == '.') { + filename = filename.substring_view(0, filename.length() - 1); + } + } + + if (filename.length() > buffer.size()) { + // FIXME: Rock Ridge allows filenames up to 255 characters, so we should + // probably support that instead of truncating. + filename = filename.substring_view(0, buffer.size()); + } + + for (size_t i = 0; i < filename.length(); i++) { + buffer[i] = to_ascii_lowercase(filename[i]); + } + + return { buffer.data(), filename.length() }; +} + +InodeIndex ISO9660Inode::get_inode_index(ISO::DirectoryRecordHeader const& record, StringView const& name) +{ + if (name.is_null()) { + // NOTE: This is the index of the root inode. + return 1; + } + + return { pair_int_hash(LittleEndian { record.extent_location.little }, string_hash(name.characters_without_null_termination(), name.length())) }; +} + +} diff --git a/Kernel/FileSystem/ISO9660FileSystem.h b/Kernel/FileSystem/ISO9660FileSystem.h new file mode 100644 index 0000000000..65a160cd44 --- /dev/null +++ b/Kernel/FileSystem/ISO9660FileSystem.h @@ -0,0 +1,390 @@ +/* + * Copyright (c) 2021, sin-ack + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Kernel { + +namespace ISO { + +// The implemented spec here is ECMA 119, available at: +// https://www.ecma-international.org/wp-content/uploads/ECMA-119_4th_edition_june_2019.pdf + +template +struct [[gnu::packed]] LittleAndBigEndian { + T little; + T big; +}; + +// 8.4.26.1 Date and Time Format +struct [[gnu::packed]] AsciiDateAndTime { + // All of these fields are ASCII digits. :^) + u8 year[4]; + u8 month[2]; + u8 day[2]; + + u8 hour[2]; + u8 minute[2]; + u8 second[2]; + u8 hundredths_of_second[2]; + + // From OSDev wiki: + // Time zone offset from GMT in 15 minute intervals, starting at + // interval -48 (west) and running up to interval 52 (east). So value 0 + // indicates interval -48 which equals GMT-12 hours, and value 100 + // indicates interval 52 which equals GMT+13 hours. + u8 timezone_offset; +}; +static_assert(sizeof(AsciiDateAndTime) == 17); + +// 9.1.5 Recording Date and Time (BP 19 to 25) +struct [[gnu::packed]] NumericalDateAndTime { + u8 years_since_1900; + u8 month; + u8 day; + u8 hour; + u8 minute; + u8 second; + // Same format as AsciiDateAndTime. + u8 timezone_offset; +}; +static_assert(sizeof(NumericalDateAndTime) == 7); + +// --- Path Table --- + +// 9.4 Format of a Path Table Record +struct [[gnu::packed]] PathTableRecord { + u8 directory_identifier_length; + u8 extended_attribute_record_length; + u32 extent_location; + u16 parent_directory_number; + + u8 directory_identifier[]; +}; +static_assert(sizeof(PathTableRecord) == 8); + +// --- Extended Attribute Record --- + +// 9.5.3 Permissions +enum class ExtendedPermissions : u16 { + SystemGroupReadable = 1 << 0, + SystemGroupExecutable = 1 << 2, + UserReadable = 1 << 4, + UserExecutable = 1 << 6, + GroupReadable = 1 << 8, + GroupExecutable = 1 << 10, + OtherReadable = 1 << 12, + OtherExecutable = 1 << 14, +}; +AK_ENUM_BITWISE_OPERATORS(ExtendedPermissions); + +// 9.5.8 Record Format +enum class RecordFormat : u8 { + NotSpecified = 0, + FixedLengthRecords = 1, + LittleEndianVariableRecords = 2, + BigEndianVariableRecords = 3, + // 4-127 are reserved for future standardization. + // 128-255 are reserved for system use. +}; + +// 9.5.9 Record Attributes +enum class RecordAttributes : u8 { + // This value means the record is stored like: \n123456\r. + LfCrDelimited = 0, + FortranVerticalSpacing = 1, + ContainsControlInformation = 2, + // 3-255 are reserved for future standardization. +}; + +// 9.5 Format of an Extended Attribute Record +struct [[gnu::packed]] ExtendedAttributeRecord { + LittleAndBigEndian owner_identification; + LittleAndBigEndian group_identification; + ExtendedPermissions permissions; + + AsciiDateAndTime file_creation_date_and_time; + AsciiDateAndTime file_modification_date_and_time; + AsciiDateAndTime file_expiration_date_and_time; + AsciiDateAndTime file_effective_date_and_time; + + RecordFormat record_format; + u8 record_attributes; + + LittleAndBigEndian record_length; + + u8 system_identifier[32]; + u8 system_use[64]; + + u8 extended_attribute_record_version; + u8 escape_sequence_length; + + u8 reserved[64]; + + LittleAndBigEndian application_use_length; + + // NOTE: Application use is immediately followed by escape sequences (no + // padding). + u8 application_use_and_escape_sequences[]; +}; +static_assert(sizeof(ExtendedAttributeRecord) == 250); + +// --- Files and Directories --- + +// 9.1.6 File Flags +enum class FileFlags : u8 { + Hidden = 1 << 0, // The "existence" flag + Directory = 1 << 1, + AssociatedFile = 1 << 2, + Record = 1 << 3, + Protection = 1 << 4, + // 5 and 6 are reserved. + MultiExtent = 1 << 7, +}; + +AK_ENUM_BITWISE_OPERATORS(FileFlags); + +struct [[gnu::packed]] DirectoryRecordHeader { + u8 length; + u8 extended_attribute_record_length; + LittleAndBigEndian extent_location; + LittleAndBigEndian data_length; + NumericalDateAndTime recording_date_and_time; + FileFlags file_flags; + u8 file_unit_size; + u8 interleave_gap_size; + LittleAndBigEndian volume_sequence_number; + u8 file_identifier_length; + + // NOTE: The file identifier itself is of variable length, so it and the + // fields following it are not included in this struct. Instead, they are: + // + // 34 to (33+file_identifier_length) - file identifier + // 1 byte of padding, if file_identifier_length is even + // + // The remaining bytes are system use (ISO9660 extensions). +}; +static_assert(sizeof(DirectoryRecordHeader) == 33); + +// --- Volume Descriptors --- + +enum class VolumeDescriptorType : u8 { + BootRecord = 0, + PrimaryVolumeDescriptor = 1, + SupplementaryOrEnhancedVolumeDescriptor = 2, + VolumePartitionDescriptor = 3, + // 4-254 are reserved. + VolumeDescriptorSetTerminator = 255, +}; + +// 8.1 Format of a Volume Descriptor +struct [[gnu::packed]] VolumeDescriptorHeader { + VolumeDescriptorType type; + // NOTE: Contains exactly "CD001". + u8 identifier[5]; + u8 version; +}; +static_assert(sizeof(VolumeDescriptorHeader) == 7); + +// 8.2 Boot Record +struct [[gnu::packed]] BootRecord { + VolumeDescriptorHeader header; + u8 boot_system_identifier[32]; + u8 boot_identifier[32]; + u8 boot_system_use[1977]; +}; +static_assert(sizeof(BootRecord) == 2048); + +// 8.3 Volume Descriptor Set Terminator +struct [[gnu::packed]] VolumeDescriptorSetTerminator { + VolumeDescriptorHeader header; + u8 zeros[2041]; +}; +static_assert(sizeof(VolumeDescriptorSetTerminator) == 2048); + +// 8.4 Primary Volume Descriptor +struct [[gnu::packed]] PrimaryVolumeDescriptor { + VolumeDescriptorHeader header; + u8 unused1; + u8 system_identifier[32]; + u8 volume_identifier[32]; + u64 unused2; + LittleAndBigEndian volume_space_size; + u8 unused3[32]; + LittleAndBigEndian volume_set_size; + LittleAndBigEndian volume_sequence_number; + LittleAndBigEndian logical_block_size; + LittleAndBigEndian path_table_size; + + u32 l_path_table_occurrence_location; + u32 l_path_table_optional_occurrence_location; + u32 m_path_table_occurrence_location; + u32 m_path_table_optional_occurrence_location; + + DirectoryRecordHeader root_directory_record_header; + u8 root_directory_identifier; // Exactly 0x00. + + u8 volume_set_identifier[128]; + u8 publisher_identifier[128]; + u8 data_preparer_identifier[128]; + u8 application_identifier[128]; + + u8 copyright_file_identifier[37]; + u8 abstract_file_identifier[37]; + u8 bibliographic_file_identifier[37]; + + AsciiDateAndTime volume_creation_date_and_time; + AsciiDateAndTime volume_modification_date_and_time; + AsciiDateAndTime volume_expiration_date_and_time; + AsciiDateAndTime volume_effective_date_and_time; + + u8 file_structure_version; // Always 0x01. + u8 unused4; + u8 application_use[512]; + u8 reserved[653]; +}; +static_assert(sizeof(PrimaryVolumeDescriptor) == 2048); + +// 8.6 Volume Partition Descriptor +struct [[gnu::packed]] VolumePartitionDescriptor { + VolumeDescriptorHeader header; + u8 unused; + + u8 system_identifier[32]; + u8 volume_partition_identifier[32]; + LittleAndBigEndian volume_partition_location; + LittleAndBigEndian volume_partition_size; + + u8 system_use[1960]; +}; +static_assert(sizeof(VolumePartitionDescriptor) == 2048); + +} + +class ISO9660Inode; +class ISO9660DirectoryIterator; + +class ISO9660FS final : public BlockBasedFileSystem { + friend ISO9660Inode; + friend ISO9660DirectoryIterator; + +public: + struct DirectoryEntry : public RefCounted { + u32 extent { 0 }; + u32 length { 0 }; + + // NOTE: This can never be empty if we read the directory successfully. + // We need it as an OwnPtr to default-construct this struct. + OwnPtr blocks; + + static KResultOr> try_create(u32 extent, u32 length, OwnPtr blocks) + { + auto result = adopt_ref_if_nonnull(new (nothrow) DirectoryEntry(extent, length, move(blocks))); + if (!result) { + return ENOMEM; + } + return result.release_nonnull(); + } + + private: + DirectoryEntry(u32 extent, u32 length, OwnPtr blocks) + : extent(extent) + , length(length) + , blocks(move(blocks)) + { + } + }; + + static KResultOr> try_create(FileDescription&); + + virtual ~ISO9660FS() override; + virtual bool initialize() override; + virtual StringView class_name() const override { return "ISO9660FS"; } + virtual Inode& root_inode() override; + + virtual unsigned total_block_count() const override; + virtual unsigned total_inode_count() const override; + + virtual u8 internal_file_type_to_directory_entry_type(DirectoryEntryView const& entry) const override; + + KResultOr> directory_entry_for_record(Badge, ISO::DirectoryRecordHeader const* record); + +private: + ISO9660FS(FileDescription&); + + KResult parse_volume_set(); + KResult create_root_inode(); + KResult calculate_inode_count() const; + + u32 calculate_directory_entry_cache_key(ISO::DirectoryRecordHeader const&); + + KResult visit_directory_record(ISO::DirectoryRecordHeader const& record, Function(ISO::DirectoryRecordHeader const*)> const& visitor) const; + + OwnPtr m_primary_volume; + RefPtr m_root_inode; + + mutable u32 m_cached_inode_count { 0 }; + HashMap> m_directory_entry_cache; +}; + +class ISO9660Inode final : public Inode { + friend ISO9660FS; + +public: + virtual ~ISO9660Inode() override; + + // ^Inode + virtual KResultOr read_bytes(off_t, size_t, UserOrKernelBuffer& buffer, FileDescription*) const override; + virtual InodeMetadata metadata() const override; + virtual KResult traverse_as_directory(Function) const override; + virtual RefPtr lookup(StringView name) override; + virtual void flush_metadata() override; + virtual KResultOr write_bytes(off_t, size_t, const UserOrKernelBuffer& buffer, FileDescription*) override; + virtual KResultOr> create_child(StringView name, mode_t, dev_t, uid_t, gid_t) override; + virtual KResult add_child(Inode&, const StringView& name, mode_t) override; + virtual KResult remove_child(const StringView& name) override; + virtual KResult chmod(mode_t) override; + virtual KResult chown(uid_t, gid_t) override; + virtual KResult truncate(u64) override; + virtual KResult set_atime(time_t) override; + virtual KResult set_ctime(time_t) override; + virtual KResult set_mtime(time_t) override; + virtual void one_ref_left() override; + +private: + // HACK: The base ISO 9660 standard says the maximum filename length is 37 + // bytes large; however, we can read filenames longer than that right now + // without any problems, so let's allow it anyway. + static constexpr size_t max_file_identifier_length = 256 - sizeof(ISO::DirectoryRecordHeader); + + ISO9660Inode(ISO9660FS&, ISO::DirectoryRecordHeader const& record, StringView const& name); + static KResultOr> try_create_from_directory_record(ISO9660FS&, ISO::DirectoryRecordHeader const& record, StringView const& name); + + static InodeIndex get_inode_index(ISO::DirectoryRecordHeader const& record, StringView const& name); + static StringView get_normalized_filename(ISO::DirectoryRecordHeader const& record, Bytes buffer); + + void create_metadata(); + time_t parse_numerical_date_time(ISO::NumericalDateAndTime const&); + + InodeMetadata m_metadata; + ISO::DirectoryRecordHeader m_record; +}; + +} + +using Kernel::ISO::has_any_flag; +using Kernel::ISO::has_flag; diff --git a/Kernel/Syscalls/mount.cpp b/Kernel/Syscalls/mount.cpp index 5bfcb5fec3..2591aa9976 100644 --- a/Kernel/Syscalls/mount.cpp +++ b/Kernel/Syscalls/mount.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -98,6 +99,21 @@ KResultOr Process::sys$mount(Userspace fs = SysFS::create(); } else if (fs_type == "tmp"sv || fs_type == "TmpFS"sv) { fs = TmpFS::create(); + } else if (fs_type == "iso9660"sv || fs_type == "ISO9660FS"sv) { + if (description.is_null()) + return EBADF; + if (!description->file().is_seekable()) { + dbgln("mount: this is not a seekable file"); + return ENODEV; + } + + dbgln("mount: attempting to mount {} on {}", description->absolute_path(), target); + + auto maybe_fs = ISO9660FS::try_create(*description); + if (maybe_fs.is_error()) { + return maybe_fs.error(); + } + fs = maybe_fs.release_value(); } else { return ENODEV; } diff --git a/Meta/CMake/all_the_debug_macros.cmake b/Meta/CMake/all_the_debug_macros.cmake index bf5c102a80..0bb7a3694b 100644 --- a/Meta/CMake/all_the_debug_macros.cmake +++ b/Meta/CMake/all_the_debug_macros.cmake @@ -88,6 +88,8 @@ set(IPV4_DEBUG ON) set(IPV4_SOCKET_DEBUG ON) set(IRC_DEBUG ON) set(IRQ_DEBUG ON) +set(ISO9660_DEBUG ON) +set(ISO9660_VERY_DEBUG ON) set(ITEM_RECTS_DEBUG ON) set(JOB_DEBUG ON) set(JPG_DEBUG ON)