blob: 921c744f797113690cb15b1b02629d063c8f09e8 [file] [log] [blame]
/*******************************************************************************
* You may amend and distribute as you like, but don't remove this header!
*
* EPPlus provides server-side generation of Excel 2007/2010 spreadsheets.
* See http://www.codeplex.com/EPPlus for details.
*
* Copyright (C) 2011 Jan Källman
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* The GNU Lesser General Public License can be viewed at http://www.opensource.org/licenses/lgpl-license.php
* If you unfamiliar with this license or have questions about it, here is an http://www.gnu.org/licenses/gpl-faq.html
*
* All code and executables are provided "as is" with no warranty either express or implied.
* The author accepts no liability for any damage or loss of business that this product may cause.
*
* Code change notes:
*
* Author Change Date
*******************************************************************************
* Jan Källman Added 25-Oct-2012
*******************************************************************************/
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Xml;
using OfficeOpenXml.Utils;
namespace OfficeOpenXml.Packaging;
/// <summary>
/// Specifies whether the target is inside or outside the System.IO.Packaging.Package.
/// </summary>
public enum TargetMode {
/// <summary>
/// The relationship references a part that is inside the package.
/// </summary>
Internal = 0,
/// <summary>
/// The relationship references a resource that is external to the package.
/// </summary>
External = 1,
}
/// <summary>
/// Represent an OOXML Zip package.
/// </summary>
internal class ZipPackage : ZipPackageRelationshipBase {
internal class ContentType {
internal string Name;
internal bool IsExtension;
internal string Match;
public ContentType(string name, bool isExtension, string match) {
Name = name;
IsExtension = isExtension;
Match = match;
}
}
private readonly Dictionary<string, ZipPackagePart> Parts = new(
StringComparer.InvariantCultureIgnoreCase);
internal Dictionary<string, ContentType> _contentTypes = new(
StringComparer.InvariantCultureIgnoreCase);
internal ZipPackage() {
AddNew();
}
private void AddNew() {
_contentTypes.Add("xml", new(ExcelPackage._schemaXmlExtension, true, "xml"));
_contentTypes.Add("rels", new(ExcelPackage._schemaRelsExtension, true, "rels"));
}
internal ZipPackage(Stream stream) {
bool hasContentTypeXml = false;
if (stream == null || stream.Length == 0) {
AddNew();
} else {
var rels = new Dictionary<string, ZipArchiveEntry>();
stream.Seek(0, SeekOrigin.Begin);
using var zip = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: true);
foreach (var e in zip.Entries) {
if (e.Length > 0) {
if (e.FullName.Equals(
"[content_types].xml",
StringComparison.InvariantCultureIgnoreCase)) {
using var inputStream = e.Open();
AddContentTypes(inputStream);
hasContentTypeXml = true;
} else if (e.FullName.Equals(
"_rels/.rels",
StringComparison.InvariantCultureIgnoreCase)) {
using var inputStream = e.Open();
ReadRelation(inputStream, "");
} else {
if (e.FullName.EndsWith(".rels", StringComparison.InvariantCultureIgnoreCase)) {
rels.Add(GetUriKey(e.FullName), e);
} else {
var data = new byte[e.Length];
using var inputStream = e.Open();
inputStream.ReadExactly(data);
var part = new ZipPackagePart(
this,
e,
ImmutableCollectionsMarshal.AsImmutableArray(data));
Parts.Add(GetUriKey(e.FullName), part);
}
}
}
}
foreach (var p in Parts) {
string name = Path.GetFileName(p.Key);
string extension = Path.GetExtension(p.Key);
string relFile = string.Format(
"{0}_rels/{1}.rels",
p.Key.Substring(0, p.Key.Length - name.Length),
name);
if (rels.TryGetValue(relFile, out var zipArchiveEntry)) {
using var inputStream = zipArchiveEntry.Open();
p.Value.ReadRelation(inputStream, p.Value.Uri.OriginalString);
}
if (_contentTypes.TryGetValue(p.Key, out var type)) {
p.Value.ContentType = type.Name;
} else if (extension.Length > 1 && _contentTypes.ContainsKey(extension.Substring(1))) {
p.Value.ContentType = _contentTypes[extension.Substring(1)].Name;
}
}
if (!hasContentTypeXml) {
throw new InvalidDataException("The file is not an valid Package file.");
}
}
}
private void AddContentTypes(Stream inputStream) {
var doc = new XmlDocument();
XmlHelper.LoadXmlSafe(doc, inputStream);
foreach (XmlElement c in doc.DocumentElement.ChildNodes) {
ContentType ct;
if (string.IsNullOrEmpty(c.GetAttribute("Extension"))) {
ct = new(c.GetAttribute("ContentType"), false, c.GetAttribute("PartName"));
} else {
ct = new(c.GetAttribute("ContentType"), true, c.GetAttribute("Extension"));
}
_contentTypes.Add(GetUriKey(ct.Match), ct);
}
}
internal void CreatePart(Uri partUri, string contentType, Action<StreamWriter> saveHandler) {
if (PartExists(partUri)) {
throw (new InvalidOperationException("Part already exist"));
}
var part = new ZipPackagePart(this, partUri, contentType, saveHandler);
_contentTypes.Add(
GetUriKey(part.Uri.OriginalString),
new(contentType, false, part.Uri.OriginalString));
Parts.Add(GetUriKey(part.Uri.OriginalString), part);
}
internal ZipPackagePart GetPart(Uri partUri) {
if (PartExists(partUri)) {
return Parts
.Single(x =>
x.Key.Equals(
GetUriKey(partUri.OriginalString),
StringComparison.InvariantCultureIgnoreCase))
.Value;
}
throw (new InvalidOperationException("Part does not exist."));
}
internal string GetUriKey(string uri) {
string ret = uri;
if (ret[0] != '/') {
ret = "/" + ret;
}
return ret;
}
internal bool PartExists(Uri partUri) {
var uriKey = GetUriKey(partUri.OriginalString.ToLower(CultureInfo.InvariantCulture));
return Parts.Keys.Any(x => x.Equals(uriKey, StringComparison.InvariantCultureIgnoreCase));
}
internal void DeletePart(Uri uri) {
var delList = new List<object[]>();
foreach (var p in Parts.Values) {
foreach (var r in p.GetRelationships()) {
if (UriHelper
.ResolvePartUri(p.Uri, r.TargetUri)
.OriginalString.Equals(
uri.OriginalString,
StringComparison.InvariantCultureIgnoreCase)) {
delList.Add(new object[] { r.Id, p });
}
}
}
foreach (var o in delList) {
((ZipPackagePart)o[1]).DeleteRelationship(o[0].ToString());
}
var rels = GetPart(uri).GetRelationships();
while (rels.Count > 0) {
rels.Remove(rels.First().Id);
}
_contentTypes.Remove(GetUriKey(uri.OriginalString));
//remove all relations
Parts.Remove(GetUriKey(uri.OriginalString));
}
internal void Save(Stream stream) {
using var zipArchive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true);
// Content types
var contentTypesEntry = zipArchive.CreateEntry("[Content_Types].xml");
using (var contentTypesWriter = new StreamWriter(contentTypesEntry.Open())) {
contentTypesWriter.Write(GetContentTypeXml());
}
// Top Rels
_rels.WriteZip(zipArchive, "_rels/.rels");
ZipPackagePart ssPart = null;
foreach (var part in Parts.Values) {
if (part.ContentType != ExcelPackage._contentTypeSharedString) {
part.WriteZip(zipArchive);
} else {
ssPart = part;
}
}
//Shared strings must be saved after all worksheets. The ss dictionary is populated when that workheets are saved (to get the best performance).
if (ssPart != null) {
ssPart.WriteZip(zipArchive);
}
}
private string GetContentTypeXml() {
StringBuilder xml = new StringBuilder(
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">");
foreach (ContentType ct in _contentTypes.Values) {
if (ct.IsExtension) {
xml.Append($"<Default ContentType=\"{ct.Name}\" Extension=\"{ct.Match}\"/>");
} else {
xml.Append($"<Override ContentType=\"{ct.Name}\" PartName=\"{GetUriKey(ct.Match)}\" />");
}
}
xml.Append("</Types>");
return xml.ToString();
}
}