From nobody Mon Nov 25 09:31:03 2024 Delivered-To: importer@patchew.org Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass(p=none dis=none) header.from=gmail.com ARC-Seal: i=1; a=rsa-sha256; t=1716717505; cv=none; d=zohomail.com; s=zohoarc; b=G1cHMT/RUC+v7dGFIRPNXt80kcM0wApfPpvuiByEfxfxPHBC6ZkGyk9Ih2MqnLPlQSeOXOS+0E5USIxUCj76J5r9dFV77CPRRO9l6nrPG+WWkMZYdIPhsiLe/ihvOhQdRr6M8ogVTPXjwO36Mj7/UqD23HsAW35niLmRlD/GLdM= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1716717505; h=Content-Transfer-Encoding:Cc:Cc:Date:Date:From:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Archive:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:Subject:To:To:Message-Id:Reply-To; bh=tSbMFsmslcfQACtPzQzMghuQaxr5NDYpNXTcVsfGjAU=; b=Gn3e2mYUWmQPwQRyGk2EZZ4T5VK0xOV2sVc73wjKC25K7lz0UG4ZkNMH+tEDYFwY+USr3MSJkEdSlByMj01+u3PsldQX6z/XT/dYooIChbg+nescIKlNkFDavj23yZHih6eNyZLdca02rMp8pBUlpgJKkDZEM+1fZYWiMKgXrwk= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass header.from= (p=none dis=none) Return-Path: Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 171671750586012.976301716336138; Sun, 26 May 2024 02:58:25 -0700 (PDT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1sBAce-0006KQ-6E; Sun, 26 May 2024 05:57:00 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1sBAcc-0006JR-Hl; Sun, 26 May 2024 05:56:58 -0400 Received: from mail-ot1-x336.google.com ([2607:f8b0:4864:20::336]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1sBAcV-0003pc-V0; Sun, 26 May 2024 05:56:58 -0400 Received: by mail-ot1-x336.google.com with SMTP id 46e09a7af769-6f855b2499cso1675970a34.1; Sun, 26 May 2024 02:56:50 -0700 (PDT) Received: from amjad-pc.. ([192.228.203.229]) by smtp.gmail.com with ESMTPSA id 41be03b00d2f7-682227f1838sm4021368a12.46.2024.05.26.02.56.47 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sun, 26 May 2024 02:56:48 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1716717409; x=1717322209; darn=nongnu.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=tSbMFsmslcfQACtPzQzMghuQaxr5NDYpNXTcVsfGjAU=; b=aWBJ/uAqPQ3Bbi9FmhUAwdQ6g5K8/2O2QpfcVCmvUhGRxrva5coQseaNRzlh9lB2WT pjXmaU1O1x/lGMhsJjO7qkWgET34juRgT93AwLviqFgULFNAD+BleUwIM+/F4nMaZeVv uWsPID9pQvnx+Z/qPqJgHsrRse1CRxxFEIpZZr2+8XLgOARZr8rWdpcag3iRCd4SQPm7 98kPjbZmiHogTRQotwVQCDjpDHS1xXbZvqyFYn47yDdAT3TT3f9Jmon4morwj/cbSjzj ZuqTseSX8H9UXL1vPs7z3ScENx5Kv0HEhONMrW/2KxlUK+QJAj/eoU6Z445+axOG/+sG Khzw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1716717409; x=1717322209; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=tSbMFsmslcfQACtPzQzMghuQaxr5NDYpNXTcVsfGjAU=; b=XCVeZHdC+PcZHhVkjRDpPZbyv46WHkz9Dy5gDkLzdwbXhwWGheOfR0meQDgbWHdnE6 Y2jjcYEWLDunyuJyI+tOKiGuKp048cL/HdM2pu4ghVj0YppNItqzE7hJJOsefOGztrff R5aHAnW9Y8px1Y5A4UenAGFW/jm95zqdu1rzJLmvwaUgcAWhe0LwBGtom/CJkPnP2edP vrOQndmsQEEQsQyvRFPZEswDC1lks5whMyPnevyZmaKtjKfOqam5DAkGCvUV+dJoDncR FsbP+kuuCwJ7uZXmZVquD3fJW9Au8s0V9U0uFbJSMBLuaLUrlCs8cU4EiH9cooZqVAi9 zNVA== X-Forwarded-Encrypted: i=1; AJvYcCW510tut0Id2d4bOzBg6YuYdTpd6Gq60tzT/UrscrWaAGikejHDAB71pUqo/obVQtbSlN+JXArHI2tEo2o0fxXCjNMfCbA= X-Gm-Message-State: AOJu0Ywg5D6VBpl0A3lxig0lH4qiD8KAWP3O9imtkR1DATepi18yHT06 SZxSep1WTvnQYwklvv3la56EJRF3e8XRZItxk9JPstbUWqZfCfrVUtxHm3l+ X-Google-Smtp-Source: AGHT+IFbt8X2EKHda3ZHbJUVt2P6mPBbnXa9Uu7sY0sFEKedg8bgJO5YFPN1B6CULm46nWMSY8Xhhw== X-Received: by 2002:a05:6830:148d:b0:6f0:e470:1915 with SMTP id 46e09a7af769-6f8d0b22c30mr7300501a34.24.1716717409154; Sun, 26 May 2024 02:56:49 -0700 (PDT) From: Amjad Alsharafi To: qemu-devel@nongnu.org Cc: Hanna Reitz , Kevin Wolf , "open list:vvfat" , Amjad Alsharafi Subject: [PATCH v3 6/6] iotests: Add `create_file` test for `vvfat` driver Date: Sun, 26 May 2024 17:56:06 +0800 Message-ID: <3e3e3887ddd9f95cba952fad6b5fb1040e126140.1716717181.git.amjadsharafi10@gmail.com> X-Mailer: git-send-email 2.45.0 In-Reply-To: References: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Received-SPF: pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) client-ip=209.51.188.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists.gnu.org; Received-SPF: pass client-ip=2607:f8b0:4864:20::336; envelope-from=amjadsharafi10@gmail.com; helo=mail-ot1-x336.google.com X-Spam_score_int: -17 X-Spam_score: -1.8 X-Spam_bar: - X-Spam_report: (-1.8 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, FREEMAIL_ENVFROM_END_DIGIT=0.25, FREEMAIL_FROM=0.001, RCVD_IN_DNSWL_NONE=-0.0001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: qemu-devel-bounces+importer=patchew.org@nongnu.org X-ZohoMail-DKIM: pass (identity @gmail.com) X-ZM-MESSAGEID: 1716717507701100002 Content-Type: text/plain; charset="utf-8" We test the ability to create new files in the filesystem, this is done by adding an entry in the desired directory list. The file will also be created in the host filesystem with matching filename. Signed-off-by: Amjad Alsharafi --- tests/qemu-iotests/fat16.py | 124 +++++++++++++++++++++++++++-- tests/qemu-iotests/tests/vvfat | 29 +++++-- tests/qemu-iotests/tests/vvfat.out | 4 +- 3 files changed, 144 insertions(+), 13 deletions(-) diff --git a/tests/qemu-iotests/fat16.py b/tests/qemu-iotests/fat16.py index 6ac5508d8d..e86bdd0b10 100644 --- a/tests/qemu-iotests/fat16.py +++ b/tests/qemu-iotests/fat16.py @@ -16,9 +16,11 @@ # along with this program. If not, see . =20 from typing import List +import string =20 SECTOR_SIZE =3D 512 DIRENTRY_SIZE =3D 32 +ALLOWED_FILE_CHARS =3D set("!#$%&'()-@^_`{}~" + string.digits + string.asc= ii_uppercase) =20 =20 class MBR: @@ -265,7 +267,7 @@ def write_fat_entry(self, cluster: int, value: int): + self.fats[fat_offset + 2 :] ) self.fats_dirty_sectors.add(fat_offset // SECTOR_SIZE) - =20 + def flush_fats(self): """ Write the FATs back to the disk. @@ -293,7 +295,7 @@ def next_cluster(self, cluster: int) -> int | None: raise Exception("Invalid FAT entry") else: return fat_entry - =20 + def next_free_cluster(self) -> int: """ Find the next free cluster. @@ -338,6 +340,67 @@ def read_directory(self, cluster: int) -> List[FatDire= ctoryEntry]: cluster =3D self.next_cluster(cluster) return entries =20 + def add_direntry(self, cluster: int | None, name: str, ext: str, attri= butes: int): + """ + Add a new directory entry to the given cluster. + If the cluster is `None`, then it will be added to the root direct= ory. + """ + + def find_free_entry(data: bytes): + for i in range(0, len(data), DIRENTRY_SIZE): + entry =3D data[i : i + DIRENTRY_SIZE] + if entry[0] =3D=3D 0 or entry[0] =3D=3D 0xE5: + return i + return None + + assert len(name) <=3D 8, "Name must be 8 characters or less" + assert len(ext) <=3D 3, "Ext must be 3 characters or less" + assert attributes % 0x15 !=3D 0x15, "Invalid attributes" + + # initial dummy data + new_entry =3D FatDirectoryEntry(b"\0" * 32, 0, 0) + new_entry.name =3D name.ljust(8, " ") + new_entry.ext =3D ext.ljust(3, " ") + new_entry.attributes =3D attributes + new_entry.reserved =3D 0 + new_entry.create_time_tenth =3D 0 + new_entry.create_time =3D 0 + new_entry.create_date =3D 0 + new_entry.last_access_date =3D 0 + new_entry.last_mod_time =3D 0 + new_entry.last_mod_date =3D 0 + new_entry.cluster =3D self.next_free_cluster() + new_entry.size_bytes =3D 0 + + # mark as EOF + self.write_fat_entry(new_entry.cluster, 0xFFFF) + + if cluster is None: + for i in range(self.boot_sector.root_dir_size()): + sector_data =3D self.read_sectors( + self.boot_sector.root_dir_start() + i, 1 + ) + offset =3D find_free_entry(sector_data) + if offset is not None: + new_entry.sector =3D self.boot_sector.root_dir_start()= + i + new_entry.offset =3D offset + self.update_direntry(new_entry) + return new_entry + else: + while cluster is not None: + data =3D self.read_cluster(cluster) + offset =3D find_free_entry(data) + if offset is not None: + new_entry.sector =3D self.boot_sector.first_sector_of_= cluster( + cluster + ) + (offset // SECTOR_SIZE) + new_entry.offset =3D offset % SECTOR_SIZE + self.update_direntry(new_entry) + return new_entry + cluster =3D self.next_cluster(cluster) + + raise Exception("No free directory entries") + def update_direntry(self, entry: FatDirectoryEntry): """ Write the directory entry back to the disk. @@ -406,9 +469,10 @@ def truncate_file(self, entry: FatDirectoryEntry, new_= size: int): raise Exception(f"{entry.whole_name()} is a directory") =20 def clusters_from_size(size: int): - return (size + self.boot_sector.cluster_bytes() - 1) // self.b= oot_sector.cluster_bytes() + return ( + size + self.boot_sector.cluster_bytes() - 1 + ) // self.boot_sector.cluster_bytes() =20 - =20 # First, allocate new FATs if we need to required_clusters =3D clusters_from_size(new_size) current_clusters =3D clusters_from_size(entry.size_bytes) @@ -438,7 +502,7 @@ def clusters_from_size(size: int): self.write_fat_entry(cluster, new_cluster) self.write_fat_entry(new_cluster, 0xFFFF) cluster =3D new_cluster - =20 + elif required_clusters < current_clusters: # Truncate the file cluster =3D entry.cluster @@ -464,7 +528,9 @@ def clusters_from_size(size: int): count +=3D 1 affected_clusters.add(cluster) cluster =3D self.next_cluster(cluster) - assert count =3D=3D required_clusters, f"Expected {required_cluste= rs} clusters, got {count}" + assert ( + count =3D=3D required_clusters + ), f"Expected {required_clusters} clusters, got {count}" =20 # update the size entry.size_bytes =3D new_size @@ -505,3 +571,49 @@ def write_file(self, entry: FatDirectoryEntry, data: b= ytes): cluster =3D self.next_cluster(cluster) =20 assert len(data) =3D=3D 0, "Data was not written completely, clust= ers missing" + + def create_file(self, path: str): + """ + Create a new file at the given path. + """ + assert path[0] =3D=3D "/", "Path must start with /" + + path =3D path[1:] # remove the leading / + + parts =3D path.split("/") + + directory_cluster =3D None + directory =3D self.read_root_directory() + + parts, filename =3D parts[:-1], parts[-1] + + for i, part in enumerate(parts): + current_entry =3D None + for entry in directory: + if entry.whole_name() =3D=3D part: + current_entry =3D entry + break + if current_entry is None: + return None + + if current_entry.attributes & 0x10 =3D=3D 0: + raise Exception(f"{current_entry.whole_name()} is not a di= rectory") + else: + directory =3D self.read_directory(current_entry.cluster) + directory_cluster =3D current_entry.cluster + + # add new entry to the directory + + filename, ext =3D filename.split(".") + + if len(ext) > 3: + raise Exception("Ext must be 3 characters or less") + if len(filename) > 8: + raise Exception("Name must be 8 characters or less") + + for c in filename + ext: + + if c not in ALLOWED_FILE_CHARS: + raise Exception("Invalid character in filename") + + return self.add_direntry(directory_cluster, filename, ext, 0) diff --git a/tests/qemu-iotests/tests/vvfat b/tests/qemu-iotests/tests/vvfat index e0e23d1ab8..d8d802589d 100755 --- a/tests/qemu-iotests/tests/vvfat +++ b/tests/qemu-iotests/tests/vvfat @@ -323,7 +323,7 @@ class TestVVFatDriver(QMPTestCase): =20 with open(os.path.join(filesystem, "file0.txt"), "rb") as f: self.assertEqual(f.read(), new_content) - =20 + def test_write_large_file(self): """ Test writing a large file @@ -342,7 +342,7 @@ class TestVVFatDriver(QMPTestCase): =20 with open(os.path.join(filesystem, "large1.txt"), "rb") as f: self.assertEqual(f.read(), new_content) - =20 + def test_truncate_file_change_clusters_less(self): """ Test truncating a file by reducing the number of clusters @@ -359,7 +359,6 @@ class TestVVFatDriver(QMPTestCase): with open(os.path.join(filesystem, "large1.txt"), "rb") as f: self.assertEqual(f.read(), b"A") =20 - =20 def test_write_file_change_clusters_less(self): """ Test truncating a file by reducing the number of clusters @@ -376,7 +375,7 @@ class TestVVFatDriver(QMPTestCase): =20 with open(os.path.join(filesystem, "large2.txt"), "rb") as f: self.assertEqual(f.read(), new_content) - =20 + def test_write_file_change_clusters_more(self): """ Test truncating a file by increasing the number of clusters @@ -392,7 +391,27 @@ class TestVVFatDriver(QMPTestCase): =20 with open(os.path.join(filesystem, "large2.txt"), "rb") as f: self.assertEqual(f.read(), new_content) - =20 + + def test_create_file(self): + """ + Test creating a new file + """ + fat16 =3D self.init_fat16() + + new_file =3D fat16.create_file("/NEWFILE.TXT") + + self.assertIsNotNone(new_file) + self.assertEqual(new_file.size_bytes, 0) + + new_content =3D b"Hello, world! New file\n" + fat16.write_file(new_file, new_content) + + self.assertEqual(fat16.read_file(new_file), new_content) + + with open(os.path.join(filesystem, "newfile.txt"), "rb") as f: + self.assertEqual(f.read(), new_content) + + # TODO: support deleting files =20 =20 if __name__ =3D=3D "__main__": diff --git a/tests/qemu-iotests/tests/vvfat.out b/tests/qemu-iotests/tests/= vvfat.out index fa16b5ccef..6323079e08 100755 --- a/tests/qemu-iotests/tests/vvfat.out +++ b/tests/qemu-iotests/tests/vvfat.out @@ -1,5 +1,5 @@ -............. +.............. ---------------------------------------------------------------------- -Ran 13 tests +Ran 14 tests =20 OK --=20 2.45.0