2019-01-24 16:43:03 +08:00
|
|
|
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
|
|
|
// See the LICENCE file in the repository root for full licence text.
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2022-06-17 15:37:17 +08:00
|
|
|
|
#nullable disable
|
|
|
|
|
|
2022-05-30 16:42:27 +08:00
|
|
|
|
using System.Buffers;
|
2017-07-26 19:22:02 +08:00
|
|
|
|
using System.Collections.Generic;
|
2016-10-05 04:29:08 +08:00
|
|
|
|
using System.IO;
|
2016-10-05 05:08:43 +08:00
|
|
|
|
using System.Linq;
|
2024-04-29 18:49:17 +08:00
|
|
|
|
using System.Text;
|
2022-05-30 16:42:27 +08:00
|
|
|
|
using Microsoft.Toolkit.HighPerformance;
|
2025-01-27 19:28:53 +08:00
|
|
|
|
using osu.Framework.Extensions;
|
2019-10-30 18:33:54 +08:00
|
|
|
|
using osu.Framework.IO.Stores;
|
2017-11-19 12:46:51 +08:00
|
|
|
|
using SharpCompress.Archives.Zip;
|
2024-04-29 18:49:17 +08:00
|
|
|
|
using SharpCompress.Common;
|
|
|
|
|
using SharpCompress.Readers;
|
2022-05-30 16:42:27 +08:00
|
|
|
|
using SixLabors.ImageSharp.Memory;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2018-02-15 11:56:22 +08:00
|
|
|
|
namespace osu.Game.IO.Archives
|
2016-10-14 11:33:58 +08:00
|
|
|
|
{
|
2018-02-15 11:56:22 +08:00
|
|
|
|
public sealed class ZipArchiveReader : ArchiveReader
|
2016-10-14 11:33:58 +08:00
|
|
|
|
{
|
2024-04-29 18:49:17 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Archives created by osu!stable still write out as Shift-JIS.
|
|
|
|
|
/// We want to force this fallback rather than leave it up to the library/system.
|
|
|
|
|
/// In the future we may want to change exports to set the zip UTF-8 flag and use that instead.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static readonly ArchiveEncoding DEFAULT_ENCODING;
|
|
|
|
|
|
2017-03-23 12:41:50 +08:00
|
|
|
|
private readonly Stream archiveStream;
|
2017-11-19 12:46:51 +08:00
|
|
|
|
private readonly ZipArchive archive;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2024-04-29 18:49:17 +08:00
|
|
|
|
static ZipArchiveReader()
|
|
|
|
|
{
|
|
|
|
|
// Required to support rare code pages.
|
|
|
|
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
|
|
|
|
|
|
|
|
|
DEFAULT_ENCODING = new ArchiveEncoding(Encoding.GetEncoding(932), Encoding.GetEncoding(932));
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-15 11:56:22 +08:00
|
|
|
|
public ZipArchiveReader(Stream archiveStream, string name = null)
|
2018-02-15 09:20:23 +08:00
|
|
|
|
: base(name)
|
2016-10-14 11:33:58 +08:00
|
|
|
|
{
|
2016-10-19 23:00:11 +08:00
|
|
|
|
this.archiveStream = archiveStream;
|
2024-04-29 18:49:17 +08:00
|
|
|
|
|
|
|
|
|
archive = ZipArchive.Open(archiveStream, new ReaderOptions
|
|
|
|
|
{
|
|
|
|
|
ArchiveEncoding = DEFAULT_ENCODING
|
|
|
|
|
});
|
2016-10-05 04:29:08 +08:00
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2016-11-05 19:00:14 +08:00
|
|
|
|
public override Stream GetStream(string name)
|
2016-10-05 04:29:08 +08:00
|
|
|
|
{
|
2017-11-19 12:46:51 +08:00
|
|
|
|
ZipArchiveEntry entry = archive.Entries.SingleOrDefault(e => e.Key == name);
|
2016-10-14 11:33:58 +08:00
|
|
|
|
if (entry == null)
|
2023-08-03 08:01:11 +08:00
|
|
|
|
return null;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2017-11-19 12:46:51 +08:00
|
|
|
|
using (Stream s = entry.OpenEntryStream())
|
2025-01-27 19:28:53 +08:00
|
|
|
|
{
|
|
|
|
|
if (entry.Size > 0)
|
|
|
|
|
{
|
|
|
|
|
var owner = MemoryAllocator.Default.Allocate<byte>((int)entry.Size);
|
|
|
|
|
s.ReadExactly(owner.Memory.Span);
|
|
|
|
|
return new MemoryOwnerMemoryStream(owner);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// due to a sharpcompress bug (https://github.com/adamhathcock/sharpcompress/issues/88),
|
|
|
|
|
// in rare instances the `ZipArchiveEntry` will not contain a correct `Size` but instead report 0.
|
|
|
|
|
// this would lead to the block above reading nothing, and the game basically seeing an archive full of empty files.
|
|
|
|
|
// since the bug is years old now, and this is a rather rare situation anyways (reported once in years),
|
|
|
|
|
// work around this locally by falling back to reading as many bytes as possible and using a standard non-pooled memory stream.
|
|
|
|
|
return new MemoryStream(s.ReadAllRemainingBytesToArray());
|
|
|
|
|
}
|
2016-10-05 04:29:08 +08:00
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2016-10-19 01:35:01 +08:00
|
|
|
|
public override void Dispose()
|
2016-10-14 11:33:58 +08:00
|
|
|
|
{
|
|
|
|
|
archive.Dispose();
|
2016-10-19 23:00:11 +08:00
|
|
|
|
archiveStream.Dispose();
|
2016-10-10 21:26:34 +08:00
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
Fix exports containing zero byte files after import from specific ZIP archives
Closes https://github.com/ppy/osu/issues/27540.
As it turns out, some ZIP archivers (like 7zip) will decide to add fake
entries for directories, and some (like windows zipfolders) won't.
The "directory" entries aren't really properly supported using any
actual data or attributes, they're detected by sharpcompress basically
by heuristics:
https://github.com/adamhathcock/sharpcompress/blob/ab5535eba365ec8fae58f92d53763ddf2dbf45af/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs#L19-L31
When importing into realm we have thus far presumed that these directory
entries will not be a thing. Having them be a thing breaks multiple
things, like:
- When importing from ZIPs with separate directory entries, a separate
`RealmFile` is created for a directory entry even though it doesn't
represent a real file
- As a result, when re-exporting a model with files imported from such
an archive, a zero-byte file would be created because to the database
it looks like it was originally a zero-byte file.
If you want to have fun, google "zip empty directories". You'll see
a whole gamut of languages, libraries, and developers stepping on this
rake. Yet another episode of underspecced mistakes from decades ago
that were somebody's "good idea" but continue to wreak havoc forevermore
because now there are two competing conventions you can't just pick one.
2024-03-12 16:06:24 +08:00
|
|
|
|
public override IEnumerable<string> Filenames => archive.Entries.Where(e => !e.IsDirectory).Select(e => e.Key).ExcludeSystemFileNames();
|
2022-05-30 16:42:27 +08:00
|
|
|
|
|
|
|
|
|
private class MemoryOwnerMemoryStream : Stream
|
|
|
|
|
{
|
|
|
|
|
private readonly IMemoryOwner<byte> owner;
|
|
|
|
|
private readonly Stream stream;
|
|
|
|
|
|
|
|
|
|
public MemoryOwnerMemoryStream(IMemoryOwner<byte> owner)
|
|
|
|
|
{
|
|
|
|
|
this.owner = owner;
|
|
|
|
|
|
|
|
|
|
stream = owner.Memory.AsStream();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override void Dispose(bool disposing)
|
|
|
|
|
{
|
|
|
|
|
owner?.Dispose();
|
|
|
|
|
base.Dispose(disposing);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void Flush() => stream.Flush();
|
|
|
|
|
|
|
|
|
|
public override int Read(byte[] buffer, int offset, int count) => stream.Read(buffer, offset, count);
|
|
|
|
|
|
|
|
|
|
public override long Seek(long offset, SeekOrigin origin) => stream.Seek(offset, origin);
|
|
|
|
|
|
|
|
|
|
public override void SetLength(long value) => stream.SetLength(value);
|
|
|
|
|
|
|
|
|
|
public override void Write(byte[] buffer, int offset, int count) => stream.Write(buffer, offset, count);
|
|
|
|
|
|
|
|
|
|
public override bool CanRead => stream.CanRead;
|
|
|
|
|
|
|
|
|
|
public override bool CanSeek => stream.CanSeek;
|
|
|
|
|
|
|
|
|
|
public override bool CanWrite => stream.CanWrite;
|
|
|
|
|
|
|
|
|
|
public override long Length => stream.Length;
|
|
|
|
|
|
|
|
|
|
public override long Position
|
|
|
|
|
{
|
|
|
|
|
get => stream.Position;
|
|
|
|
|
set => stream.Position = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-10-14 11:33:58 +08:00
|
|
|
|
}
|
2017-11-19 12:46:51 +08:00
|
|
|
|
}
|