mirror of
https://github.com/ppy/osu.git
synced 2026-05-23 11:40:28 +08:00
83e47b3c4c
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.
95 lines
2.9 KiB
C#
95 lines
2.9 KiB
C#
// 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.
|
|
|
|
#nullable disable
|
|
|
|
using System.Buffers;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Microsoft.Toolkit.HighPerformance;
|
|
using osu.Framework.IO.Stores;
|
|
using SharpCompress.Archives.Zip;
|
|
using SixLabors.ImageSharp.Memory;
|
|
|
|
namespace osu.Game.IO.Archives
|
|
{
|
|
public sealed class ZipArchiveReader : ArchiveReader
|
|
{
|
|
private readonly Stream archiveStream;
|
|
private readonly ZipArchive archive;
|
|
|
|
public ZipArchiveReader(Stream archiveStream, string name = null)
|
|
: base(name)
|
|
{
|
|
this.archiveStream = archiveStream;
|
|
archive = ZipArchive.Open(archiveStream);
|
|
}
|
|
|
|
public override Stream GetStream(string name)
|
|
{
|
|
ZipArchiveEntry entry = archive.Entries.SingleOrDefault(e => e.Key == name);
|
|
if (entry == null)
|
|
return null;
|
|
|
|
var owner = MemoryAllocator.Default.Allocate<byte>((int)entry.Size);
|
|
|
|
using (Stream s = entry.OpenEntryStream())
|
|
s.ReadExactly(owner.Memory.Span);
|
|
|
|
return new MemoryOwnerMemoryStream(owner);
|
|
}
|
|
|
|
public override void Dispose()
|
|
{
|
|
archive.Dispose();
|
|
archiveStream.Dispose();
|
|
}
|
|
|
|
public override IEnumerable<string> Filenames => archive.Entries.Where(e => !e.IsDirectory).Select(e => e.Key).ExcludeSystemFileNames();
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|