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())