From be7dd482a7f195acb3f004aa90c4ded91f6b4d98 Mon Sep 17 00:00:00 2001 From: Alex Manekovskyi <873602+manekovskiy@users.noreply.github.com> Date: Tue, 15 Apr 2025 14:24:58 -0700 Subject: [PATCH] Support for singleline stripe parsing in LVM - Add support for singleline stripe parsing in LVM - Add tests for MetadataSegmentSection class --- Library/DiscUtils.Lvm/AssemblyInfo.cs | 3 + .../DiscUtils.Lvm/MetadataSegmentSection.cs | 68 ++++++- Library/DiscUtils.Lvm/MetadataStripe.cs | 12 +- .../Lvm/MetadataSegmentSectionTests.cs | 172 ++++++++++++++++++ 4 files changed, 237 insertions(+), 18 deletions(-) create mode 100644 Library/DiscUtils.Lvm/AssemblyInfo.cs create mode 100644 Tests/LibraryTests/Lvm/MetadataSegmentSectionTests.cs diff --git a/Library/DiscUtils.Lvm/AssemblyInfo.cs b/Library/DiscUtils.Lvm/AssemblyInfo.cs new file mode 100644 index 000000000..45665c264 --- /dev/null +++ b/Library/DiscUtils.Lvm/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("LibraryTests")] \ No newline at end of file diff --git a/Library/DiscUtils.Lvm/MetadataSegmentSection.cs b/Library/DiscUtils.Lvm/MetadataSegmentSection.cs index ef1faac34..8259ecd56 100644 --- a/Library/DiscUtils.Lvm/MetadataSegmentSection.cs +++ b/Library/DiscUtils.Lvm/MetadataSegmentSection.cs @@ -51,13 +51,15 @@ internal void Parse(string head, TextReader data) if (line.AsSpan().Contains("=".AsSpan(), StringComparison.Ordinal)) { var parameter = Metadata.ParseParameter(line.AsMemory()); + var parameterValue = parameter.Value.Span; + switch (parameter.Key.ToString().ToLowerInvariant()) { case "start_extent": - StartExtent = Metadata.ParseNumericValue(parameter.Value.Span); + StartExtent = Metadata.ParseNumericValue(parameterValue); break; case "extent_count": - ExtentCount = Metadata.ParseNumericValue(parameter.Value.Span); + ExtentCount = Metadata.ParseNumericValue(parameterValue); break; case "type": var value = Metadata.ParseStringValue(parameter.Value.Span); @@ -127,12 +129,23 @@ internal void Parse(string head, TextReader data) break; case "stripe_count": - StripeCount = Metadata.ParseNumericValue(parameter.Value.Span); + StripeCount = Metadata.ParseNumericValue(parameterValue); break; case "stripes": - if (parameter.Value.Span.Equals("[".AsSpan(), StringComparison.Ordinal)) + if (parameterValue.Equals("[", StringComparison.Ordinal)) + { + // Multi-line section + Stripes = ParseMultiLineStripesSection(data).ToArray(); + } + else if (parameterValue.StartsWith("[", StringComparison.Ordinal) && parameterValue.EndsWith("]", StringComparison.Ordinal)) + { + // Single line section + // Exclude the brackets from the input + Stripes = ParseSinglelineStripesSection(parameterValue[1..^1]).ToArray(); + } + else { - Stripes = ParseStripesSection(data).ToArray(); + throw new ArgumentException("Unsupported or invalid stripe format", line); } break; @@ -151,7 +164,7 @@ internal void Parse(string head, TextReader data) } } - private static IEnumerable ParseStripesSection(TextReader data) + private static IEnumerable ParseMultiLineStripesSection(TextReader data) { string line; while ((line = Metadata.ReadLine(data)) != null) @@ -166,11 +179,48 @@ private static IEnumerable ParseStripesSection(TextReader data) yield break; } - var pv = new MetadataStripe(); - pv.Parse(line); - yield return pv; + var metadataStripes = ParseSinglelineStripesSection(line); + + foreach (var metadataStripe in metadataStripes) + { + yield return metadataStripe; + } } } + + private static List ParseSinglelineStripesSection(ReadOnlySpan data) + { + var metadataStripes = new List(); + while (!data.IsEmpty) + { + // Find the first comma separating the pair + var firstCommaIndex = data.IndexOf(','); + if (firstCommaIndex == -1) break; + + // Extract the first value + var volumeNameSpan = data.Slice(0, firstCommaIndex); + data = data[(firstCommaIndex + 1)..]; + + // Find the second comma separating the pair + var secondCommaIndex = data.IndexOf(','); + var extentNumberSpan = secondCommaIndex == -1 + ? data // Last value + : data.Slice(0, secondCommaIndex); + + // Create and parse the MetadataStripe + var metadataStripe = new MetadataStripe(); + metadataStripe.Parse(volumeNameSpan, extentNumberSpan); + + metadataStripes.Add(metadataStripe); + + // Move to the next pair + data = secondCommaIndex == -1 + ? ReadOnlySpan.Empty + : data[(secondCommaIndex + 1)..]; + } + + return metadataStripes; + } } [Flags] diff --git a/Library/DiscUtils.Lvm/MetadataStripe.cs b/Library/DiscUtils.Lvm/MetadataStripe.cs index ddf25341f..a6df90264 100644 --- a/Library/DiscUtils.Lvm/MetadataStripe.cs +++ b/Library/DiscUtils.Lvm/MetadataStripe.cs @@ -29,15 +29,9 @@ internal class MetadataStripe public string PhysicalVolumeName; public ulong StartExtentNumber; - internal void Parse(string line) + internal void Parse(ReadOnlySpan volumeNameSpan, ReadOnlySpan extentNumberSpan) { - var parts = line.Split(','); - if (parts.Length != 2) - { - throw new ArgumentException("invalid stripe format", line); - } - - PhysicalVolumeName = Metadata.ParseStringValue(parts[0].AsSpan()); - StartExtentNumber = Metadata.ParseNumericValue(parts[1].AsSpan()); + PhysicalVolumeName = Metadata.ParseStringValue(volumeNameSpan); + StartExtentNumber = Metadata.ParseNumericValue(extentNumberSpan); } } diff --git a/Tests/LibraryTests/Lvm/MetadataSegmentSectionTests.cs b/Tests/LibraryTests/Lvm/MetadataSegmentSectionTests.cs new file mode 100644 index 000000000..4135b650f --- /dev/null +++ b/Tests/LibraryTests/Lvm/MetadataSegmentSectionTests.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.IO; +using DiscUtils.Lvm; +using Xunit; + +namespace LibraryTests.Lvm; + +public class MetadataSegmentSectionTests +{ + public static IEnumerable MetadataTestCases => + [ + [ + "segment1 {", + """ + start_extent = 0 + extent_count = 2560 + type = "striped" + stripe_count = 1 + stripes = ["pv0", 3840] + } + """, + new MetadataSegmentSection + { + Name = "segment1", + StartExtent = 0, + ExtentCount = 2560, + Type = SegmentType.Striped, + StripeCount = 1, + Stripes = + [ + new MetadataStripe + { + PhysicalVolumeName = "pv0", + StartExtentNumber = 3840, + }, + ], + }, + ], + [ + "segment2 {", + """ + start_extent = 0 + extent_count = 94 + type = "striped" + stripe_count = 1 + stripes = [ + "pv0", 1813 + ] + } + """, + new MetadataSegmentSection + { + Name = "segment2", + StartExtent = 0, + ExtentCount = 94, + Type = SegmentType.Striped, + StripeCount = 1, + Stripes = + [ + new MetadataStripe + { + PhysicalVolumeName = "pv0", + StartExtentNumber = 1813, + }, + ], + }, + ], + [ + "segment3 {", + """ + start_extent = 0 + extent_count = 768 + type = "striped" + stripe_count = 2 + stripes = ["pv0", 512, "pv1", 0] + } + """, + new MetadataSegmentSection + { + Name = "segment3", + StartExtent = 0, + ExtentCount = 768, + StripeCount = 2, + Type = SegmentType.Striped, + Stripes = + [ + new MetadataStripe + { + PhysicalVolumeName = "pv0", + StartExtentNumber = 512, + }, + new MetadataStripe + { + PhysicalVolumeName = "pv1", + StartExtentNumber = 0, + }, + ], + }, + ], + [ + "segment4 {", + """ + start_extent = 0 + extent_count = 1024 + type = "striped" + stripe_count = 3 + stripes = [ + "pv0", 1280, "pv1", 768, + "pv2", 0 + ] + } + """, + new MetadataSegmentSection + { + Name = "segment4", + StartExtent = 0, + ExtentCount = 1024, + Type = SegmentType.Striped, + StripeCount = 3, + Stripes = + [ + new MetadataStripe + { + PhysicalVolumeName = "pv0", + StartExtentNumber = 1280, + }, + new MetadataStripe + { + PhysicalVolumeName = "pv1", + StartExtentNumber = 768, + }, + new MetadataStripe + { + PhysicalVolumeName = "pv2", + StartExtentNumber = 0, + }, + ], + }, + ], + ]; + + [Theory()] + [MemberData(nameof(MetadataTestCases))] + internal void ParseShouldSucceedForStripedSegment(string head, string dataString, MetadataSegmentSection expectedMetadataSegmentSection) + { + var metadataSegmentSection = new MetadataSegmentSection(); + metadataSegmentSection.Parse(head, new StringReader(dataString)); + + Assert.Equivalent(expectedMetadataSegmentSection, metadataSegmentSection); + } + + [Fact] + public void ParseShouldThrowForInvalidStripedSegment() + { + var textReader = new StringReader( + """ + start_extent = 0 + extent_count = 2560 + type = "striped" + stripe_count = 1 + stripes = ["pv0", 3840 + ] + } + """); + + var metadataSegmentSection = new MetadataSegmentSection(); + + var actualException = Assert.Throws(() => metadataSegmentSection.Parse("segment1 {", textReader)); + Assert.StartsWith("Unsupported or invalid stripe format", actualException.Message); + } +}