diff --git a/Xbim.InformationSpecifications.NewTests/IoTests.cs b/Xbim.InformationSpecifications.NewTests/IoTests.cs index 711908b..f9788d5 100644 --- a/Xbim.InformationSpecifications.NewTests/IoTests.cs +++ b/Xbim.InformationSpecifications.NewTests/IoTests.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using System.IO; +using System.IO.Compression; using System.Linq; using Xbim.InformationSpecifications.Tests.Helpers; using Xunit; @@ -143,7 +144,7 @@ public void CanLoadOptionalFacet() } - [Fact] + [Fact] public void CanLoadXml() { var f = new FileInfo(@"Files/IDS_example-with-restrictions.xml"); @@ -188,5 +189,86 @@ public void NoWarnsOnCurrentJsonVersion() LoggingTestHelper.NoIssues(loggerMock); File.Delete(filename); } + + [Fact] + public void CanSaveXmlAsZip() + { + Xids? x = BuildMultiSpecGroupIDS(); + + using var ms = new MemoryStream(); + + x.ExportBuildingSmartIDS(ms); + + // Check the stream is a PK Zip stream by looking at 'magic' first 4 bytes + Xids.IsZipped(ms).Should().BeTrue(); + } + + [Fact] + public void ZippedIDSSpecsContainsIDS() + { + Xids? x = BuildMultiSpecGroupIDS(); + + using var ms = new MemoryStream(); + + x.ExportBuildingSmartIDS(ms); + + // Check Contains IDS files & content + using var archive = new ZipArchive(ms, ZipArchiveMode.Read, false); + + archive.Entries.Should().HaveCount(2); + + archive.Entries.Should().AllSatisfy(e => e.Name.Should().EndWith(".ids", "IDS file extension expected")); + archive.Entries.Should().AllSatisfy(e => e.Length.Should().BeGreaterThan(0, "Content expected")); + } + + [Fact] + public void CanLoadXmlAsZip() + { + Xids? x = BuildMultiSpecGroupIDS(); + + using var ms = new MemoryStream(); + + x.ExportBuildingSmartIDS(ms); + ms.Position = 0; + + var newIds = Xids.LoadBuildingSmartIDS(ms); + + newIds.Should().NotBeNull(); + newIds!.SpecificationsGroups.Should().HaveCount(2); + } + + [Fact] + public void CanLoadXmlFromZipFile() + { + Xids? x = BuildMultiSpecGroupIDS(); + var tempXmlFile = Path.ChangeExtension(Path.GetTempFileName(), "zip"); + using (var fs = new FileStream(tempXmlFile, FileMode.Create)) + { + x.ExportBuildingSmartIDS(fs); + } + + var newIds = Xids.LoadBuildingSmartIDS(tempXmlFile); + + newIds.Should().NotBeNull(); + newIds!.SpecificationsGroups.Should().HaveCount(2); + } + + private static Xids BuildMultiSpecGroupIDS() + { + var file = new FileInfo(@"bsFiles/bsFilesSelf/TestFile.ids"); + var x = Xids.Load(file); + x.Should().NotBeNull(); + x!.AllSpecifications().Should().HaveCount(1); + + // Add a 2nd group with a basic spec + var specGroup = new SpecificationsGroup(x); + x.SpecificationsGroups.Add(specGroup); + + var newSpec = x.PrepareSpecification(specGroup, IfcSchemaVersion.Undefined); + newSpec.Applicability.Facets.Add(new IfcTypeFacet() { IfcType = "Door" }); + + x!.AllSpecifications().Should().HaveCount(2); + return x; + } } } diff --git a/Xbim.InformationSpecifications/Helpers/StringExtensions.cs b/Xbim.InformationSpecifications/Helpers/StringExtensions.cs index 677c6b1..b8b538b 100644 --- a/Xbim.InformationSpecifications/Helpers/StringExtensions.cs +++ b/Xbim.InformationSpecifications/Helpers/StringExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Linq; namespace Xbim.InformationSpecifications.Helpers @@ -26,5 +27,16 @@ public static string FirstCharToUpper(this string input) => _ => input.First().ToString().ToUpper() + input[1..] #endif }; + + private static char[] InvalidChars = Path.GetInvalidFileNameChars(); + /// + /// Makes a filename safe by escaping reserved characters + /// + /// + /// a Safe filename + public static string MakeSafeFileName(this string filename) + { + return InvalidChars.Aggregate(filename, (current, c) => current.Replace(c, '_')); + } } } diff --git a/Xbim.InformationSpecifications/IO/Xids.IO.cs b/Xbim.InformationSpecifications/IO/Xids.IO.cs index 0cf3194..002d0a7 100644 --- a/Xbim.InformationSpecifications/IO/Xids.IO.cs +++ b/Xbim.InformationSpecifications/IO/Xids.IO.cs @@ -30,6 +30,36 @@ public static bool CanLoad(FileInfo sourceFile, ILogger? logger = null) return false; } + const int ZipMagic = 0x04034b50; // Zip magic number: PK/003/004 + + /// + /// Determines if the stream is zipped + /// + /// + /// + /// + public static bool IsZipped(Stream stream, ILogger? logger = null) + { + if(!stream.CanSeek) + { + return false; + } + try + { + stream.Position = 0; + var bytes = new byte[4]; + stream.Read(bytes, 0, 4); + var magic = BitConverter.ToInt32(bytes, 0); + stream.Position = 0; + + return magic == ZipMagic; + } + catch + { + return false; + } + } + /// /// Loads a xids model from any of the suppoerted files. /// diff --git a/Xbim.InformationSpecifications/IO/Xids.Io.xml.cs b/Xbim.InformationSpecifications/IO/Xids.Io.xml.cs index d61aca1..53e7f52 100644 --- a/Xbim.InformationSpecifications/IO/Xids.Io.xml.cs +++ b/Xbim.InformationSpecifications/IO/Xids.Io.xml.cs @@ -5,11 +5,11 @@ using System.IO; using System.IO.Compression; using System.Linq; -using System.Runtime.InteropServices; using System.Xml; using System.Xml.Linq; using Xbim.InformationSpecifications.Cardinality; using Xbim.InformationSpecifications.Facets.buildingSMART; +using Xbim.InformationSpecifications.Helpers; namespace Xbim.InformationSpecifications { @@ -75,9 +75,10 @@ public ExportedFormat ExportBuildingSmartIDS(Stream destinationStream, ILogger? int i = 0; foreach (var specGroup in SpecificationsGroups) { - var name = (specGroup.Name is not null && !string.IsNullOrEmpty(specGroup.Name) && specGroup.Name.IndexOfAny(Path.GetInvalidFileNameChars()) < 0) - ? $"{++i} - {specGroup.Name}.xml" - : $"{++i}.xml"; + + var name = (string.IsNullOrEmpty(specGroup.Name)) + ? $"{++i}.ids" + : $"{++i} - {specGroup.Name!.MakeSafeFileName()}.ids"; var file = zipArchive.CreateEntry(name); using var str = file.Open(); using XmlWriter writer = XmlWriter.Create(str, WriteSettings); @@ -442,15 +443,48 @@ static private void WriteFacetBaseElements(FacetBase cf, XmlWriter xmlWriter) /// - /// Attempts to unpersist an XIDS from a stream. + /// Attempts to load an XIDS from a stream, where the stream is either an XML IDS or a zip file containing multiple IDS XML files /// - /// The XML source stream to parse. + /// The XML or ZIP source stream to parse. /// The logger to send any errors and warnings to. /// an XIDS or null if it could not be read. public static Xids? LoadBuildingSmartIDS(Stream stream, ILogger? logger = null) { - var t = XElement.Load(stream); - return LoadBuildingSmartIDS(t, logger); + if (IsZipped(stream)) + { + using(var zip = new ZipArchive(stream, ZipArchiveMode.Read, false)) + { + var xids = new Xids(); + foreach(var entry in zip.Entries) + { + try + { + if(entry.Name.EndsWith(".ids", StringComparison.InvariantCultureIgnoreCase)) + { + using(var idsStream = entry.Open()) + { + var element = XElement.Load(idsStream); + LoadBuildingSmartIDS(element, logger, xids); + } + } + } + catch(Exception ex) + { + logger?.LogError(ex, "Failed to load IDS file from zip stream"); + } + } + if(!xids.AllSpecifications().Any()) + { + logger?.LogWarning("No specifications found in this zip file. Ensure the zip contains *.ids files"); + } + return xids; + } + } + else + { + var t = XElement.Load(stream); + return LoadBuildingSmartIDS(t, logger); + } } /// @@ -463,7 +497,7 @@ static private void WriteFacetBaseElements(FacetBase cf, XmlWriter xmlWriter) } /// - /// Attempts to unpersist an XIDS from a file, given the file name. + /// Attempts to unpersist an XIDS from the provider IDS XML file or zip file containing IDS files. /// /// File name of the Xids to load /// The logger to send any errors and warnings to. @@ -476,12 +510,28 @@ static private void WriteFacetBaseElements(FacetBase cf, XmlWriter xmlWriter) logger?.LogError("File '{fileName}' not found from executing directory '{fullDirectoryName}'", fileName, d.FullName); return null; } - var main = XElement.Parse(File.ReadAllText(fileName)); - return LoadBuildingSmartIDS(main, logger); + if(fileName.EndsWith(".zip", StringComparison.InvariantCultureIgnoreCase)) + { + using var stream = File.OpenRead(fileName); + if(IsZipped(stream)) + { + return LoadBuildingSmartIDS(stream, logger); + } + else + { + logger?.LogError("Not a valid zip file"); + return null; + } + } + else + { + var main = XElement.Parse(File.ReadAllText(fileName)); + return LoadBuildingSmartIDS(main, logger); + } } /// - /// Should use instead. + /// Should use instead. /// [Obsolete("Use LoadBuildingSmartIDS instead.")] public static Xids? ImportBuildingSmartIDS(XElement main, ILogger? logger = null) @@ -494,12 +544,13 @@ static private void WriteFacetBaseElements(FacetBase cf, XmlWriter xmlWriter) /// /// the IDS element to load. /// the logging context + /// /// an entire new XIDS of null on errors - public static Xids? LoadBuildingSmartIDS(XElement main, ILogger? logger = null) + public static Xids? LoadBuildingSmartIDS(XElement main, ILogger? logger = null, Xids? ids = null) { if (main.Name.LocalName == "ids") { - var ret = new Xids(); + var ret = ids ?? new Xids(); var grp = new SpecificationsGroup(ret); ret.SpecificationsGroups.Add(grp); foreach (var sub in main.Elements())