Zip 文件。有一个包含许多文件的 zip 文件,但中央目录中只有一个。如何以编程方式重建中央目录。使用 JSZip

问题描述 投票:0回答:1

有一个包含许多文件的 zip 文件,但中心目录中只有一个文件。如何以编程方式重建中央目录?我的项目正在使用 JSZip 的浏览器实现。

我可以使用WinRAR的“修复”功能,并且中心目录已正确重建。我如何使用基于浏览器的 Javascript 来做到这一点?

zip jszip
1个回答
0
投票

我能提供的只是执行此操作的 C++ 代码,您可以将其转换为 Javascript。

/* ziptail.cpp -- rebuild a zip file's central directory and end records
 * Copyright (C) 2023 Mark Adler
 * Version 1.0  8 May 2023  Mark Adler
 */

/*
  This software is provided 'as-is', without any express or implied
  warranty.  In no event will the author be held liable for any damages
  arising from the use of this software.

  Permission is granted to anyone to use this software for any purpose,
  including commercial applications, and to alter it and redistribute it
  freely, subject to the following restrictions:

  1. The origin of this software must not be misrepresented; you must not
     claim that you wrote the original software. If you use this software
     in a product, an acknowledgment in the product documentation would be
     appreciated but is not required.
  2. Altered source versions must be plainly marked as such, and must not be
     misrepresented as being the original software.
  3. This notice may not be removed or altered from any source distribution.

  Mark Adler
  [email protected]
 */

// Read a zip file on stdin and write the local entries from that zip file on
// stdout followed by reconstructed central and end records. This assumes that
// the local entries on the input start at the beginning of the input and are
// contiguous. Nothing is assumed about what follows the local entries in the
// input, though if they are not followed by the start of a central directory
// record, this is noted on stderr. If that is the case, it should be assumed
// that not all of the original zip file was recovered. Any invalid data
// descriptors for deflated entries are replaced with valid data descriptors.
// If an encrypted entry, a non-deflate entry with a data descriptor, or an
// invalid entry is encountered, then the processing ends there. In that case
// the central directory and end records are written nevertheless, so that the
// result is a valid zip file. If an error is encountered in the entry's
// deflate data, then a partial entry may be written, but not included in the
// central directory. Such a zip file will still be extractable, but will have
// some wasted space before the central directory.

// If a zip file was only partially transferred, then ziptail will create a
// valid zip file for the entries that are complete.

// ziptail can be used to repair large zip files made by the macOS ditto
// command. ditto does not seem to know about the Zip64 zip format extensions,
// introduced in 2001. (More than two decades ago. Go figure.) If ditto makes a
// zip file with a central directory that starts on or after 4GB into the file,
// or if it has an entry that decompresses to 4GB or more, then that zip file
// is corrupted. ziptail will fix it.

#include <iostream>
#include <fstream>
#include <algorithm>
#include <vector>
#include <cstdint>
#include "zlib.h"
#ifdef _WIN32
#  include <fcntl.h>
#  include <io.h>
#  define read _read
#  define write _write
#  define lseek _lseeki64
#  define ftruncate _chsize_s
#  define BINARY(fd) _setmode(fd, _O_BINARY)
#else
#  include <unistd.h>
#  define BINARY(fd)
#endif

// --- Little-endian integer reads and write from memory ---

// Return the two-byte little-endian unsigned integer at ptr.
static inline uint16_t get2(uint8_t const* ptr) {
    return ptr[0] | ((uint16_t)ptr[1] << 8);
}

// Return the four-byte little-endian unsigned integer at ptr.
static inline uint32_t get4(uint8_t const* ptr) {
    return get2(ptr) | ((uint32_t)get2(ptr + 2) << 16);
}

// Return the eight-byte little-endian unsigned integer at ptr.
static inline uint64_t get8(uint8_t const* ptr) {
    return get4(ptr) | ((uint64_t)get4(ptr + 4) << 32);
}

// Put the two-byte little-endian integer val at ptr.
static inline void put2(uint8_t* ptr, uint16_t val) {
    ptr[0] = val;
    ptr[1] = val >> 8;
}

// Put the four-byte little-endian integer val at ptr.
static inline void put4(uint8_t* ptr, uint32_t val) {
    put2(ptr, val);
    put2(ptr + 2, val >> 16);
}

// Put the eight-byte little-endian integer val at ptr.
static inline void put8(uint8_t* ptr, uint64_t val) {
    put4(ptr, val);
    put4(ptr + 4, val >> 32);
}

// --- I/O functions ---

// The I/O macros are used in a context that contains a size_t got variable in
// which the number of read bytes is returned, where an int ret variable is set
// to a code in the event of an error, and where a break will disposition that
// ret value.

// For these I/O functions, the ssize_t type is avoided since Visual C doesn't
// have it. Visual C's _read() and _write() use unsigned and int instead of
// size_t and ssize_t as Posix's read() and write() do. We assume that the int
// in Visual C is more than 16 bits. Otherwise multiple calls would be needed
// for names or extra blocks of 32K or more bytes.

// Read len bytes to buf, first from strm's input, and then from in. Return the
// number of bytes actually read, which will be less than len only if the end
// of the input or a read error is encountered. If there is a read error, then
// (size_t)-1 is returned.
static inline size_t pull(z_stream* strm, int in, uint8_t* buf, size_t len) {
    size_t copy = std::min(len, (size_t)strm->avail_in);
    if (copy) {
        memcpy(buf, strm->next_in, copy);
        strm->next_in += copy;
        strm->avail_in -= copy;
        len -= copy;
        buf += copy;
    }
    while (len) {
        // read() can return less than the request. Keep reading until we get
        // len bytes or there are no more bytes to read.
        size_t got = read(in, buf, len);
        if (got == (size_t)-1)
            // Read error.
            return got;
        if (got == 0)
            // Ran into EOF before getting len bytes.
            break;
        len -= got;
        buf += got;
        copy += got;
    }
    return copy;
}

// Macro to set got and break on a pull error.
#define PULL(s, i, b, n) \
    do { \
        got = pull(s, i, b, n); \
        if (got == (size_t)-1) { \
            ret = -1; \
            break; \
        } \
    } while (0)

// Macro to set got and break on a read error.
#define READ(f, b, n) \
do { \
    got = read(f, b, n); \
    if (got == (size_t)-1) { \
        ret = -1; \
        break; \
    } \
} while (0)

// Macro to break on a write error.
#define WRITE(f, b, n) \
do { \
    if (write(f, b, n) < 0) { \
        ret = -1; \
        break; \
    } \
} while (0)

// --- Zip format functions ---

// Directory entry with the information needed to populate the central
// directory records.
typedef std::vector<uint8_t> seq_t;
struct entry {
    seq_t loc;              // local header
    seq_t name;             // local name field
    seq_t extra;            // local extra field (snipped)
    uint32_t crc;           // CRC-32
    uint64_t clen;          // compressed length
    uint64_t ulen;          // uncompressed length
    uint64_t off;           // local header offset in output
    entry(seq_t& loc, seq_t& name, seq_t& extra,
          uint32_t crc, uint64_t clen, uint64_t ulen, uint64_t off) :
          loc(loc), name(name), extra(extra),
          crc(crc), clen(clen), ulen(ulen), off(off) {}
};

// Look for an extra data field with tag tag in extra. Return the offset of the
// data field if found, -1 if not found, or -2 if the extra field structure is
// invalid.
static long extra_tag(unsigned tag, seq_t const& extra) {
    long at = -1;
    size_t i = 0;
    while (i + 4 <= extra.size()) {
        unsigned id = get2(extra.data() + i);
        unsigned size = get2(extra.data() + i + 2);
        if (id == tag) {
            if (at != -1)
                // We already found one! There can't be two of them.
                return -2;
            at = i;
        }
        i += 4 + size;
    }
    if (i != extra.size())
        // extra did not obey the required fields structure.
        return -2;
    return at;
}

// Snip the ends off of local extra data fields known to be shorter in the
// central directory. This assumes that the extra field structure has already
// been validated by extra_tag(). Delete any Zip64 extended info data field.
static void extra_snip(seq_t& extra) {
    // Local extra data fields shortened in central directory: {tag, minimum
    // length, shortened length}. 0x5455 shortening depends on a flag byte in
    // the data. If the low bit of the first byte is set, then the data is
    // shortened to five bytes, otherwise it is shortened to that one byte. See
    // proginfo/extrafld.txt in the Info-ZIP zip distribution, where all of
    // these are documented.
    static const uint16_t snip[][3] = {
        {0x0009, 10, 4},        // OS/2 extended attributes
        {0x334d, 20, 14},       // Macintosh
        {0x4453, 11, 4},        // Windows NT security descriptor
        {0x4c41, 10, 4},        // OS/2 access control list
        {0x5455, 1, 1},         // Extended timestamp (special rule)
        {0x5855, 12, 8},        // Unix type 1
        {0x6542, 5, 5},         // BeOS
        {0x7855, 6, 2}          // Unix type 2
    };
    size_t i = 0, k = 0;
    while (i + 4 <= extra.size()) {
        unsigned id = get2(extra.data() + i);
        unsigned size = get2(extra.data() + i + 2);
        unsigned keep = 4 + size;
        if (id == 1)
            // Delete the extended information data field.
            keep = 0;
        else {
            // See if id is in the snip table.
            size_t lo = 0, hi = sizeof(snip) / sizeof(snip[0]);
            while (hi - lo > 1) {
                size_t mid = (lo + hi) >> 1;
                if (snip[mid][0] > id)
                    hi = mid;
                else
                    lo = mid;
            }
            if (snip[lo][0] == id && size >= snip[lo][1]) {
                // Snip end off data field found in table.
                keep = 4 + snip[lo][2];
                if (id == 0x5455 && (extra[4] & 1) && size >= keep)
                    // Keep modified time.
                    keep += 4;
                put2(extra.data() + i + 2, keep - 4);
            }
        }
        if (k != i)
            // If we snipped something before this, move the data down.
            memmove(extra.data() + k, extra.data() + i, keep);
        k += keep;
        i += 4 + size;
    }
    extra.resize(k);        // Reduce to the snipped size.
}

// --- ziptail function ---

// ZIP format header lengths and signatures. They are: local file header, data
// descriptor, central directory header, Zip64 end of central directory record,
// Zip64 end of central directory record locator, and end of central directory
// record.
#define LOCLEN 30
#define CENLEN 46
#define Z64ENDLEN 56
#define Z64LOCLEN 20
#define ENDLEN 22
#define LOCSIG 0x04034b50
#define DATSIG 0x08074b50       // length can be 12, 16, 20, or 24
#define CENSIG 0x02014b50
#define Z64ENDSIG 0x06064b50
#define Z64LOCSIG 0x07064b50
#define ENDSIG 0x06054b50

// Maximum values for two and four-byte fields.
#define MAX16 0xffff
#define MAX32 0xffffffff

// Error codes.
static char const* errmsg[] = {
    /* -1 */  "i/o error",
    /* -2 */  "out of memory",
    /* -3 */  "unsupported zip feature: encryption",
    /* -4 */  "unsupported zip feature: non-deflate streaming",
    /* -5 */  "invalid zip file data",
    /* -6 */  "invalid compressed data"
};

// Read and copy all of the local entries from file descriptors in to out.
// Construct and write the corresponding central and end records to out, using
// the information saved from the local entries. Invalid data descriptors are
// replaced with valid data descriptors. On success, 0 is returned. Otherwise
// a negative error code is returned, enumerated above in errmsg[].
static int zip_tail(int in, int out) {
    uint64_t pos = 0, off;
    z_stream strm = {};
    int ret = inflateInit2(&strm, -15);
    if (ret != Z_OK)
        // Out of memory.
        return -2;
    uint8_t* comp[16384];
    std::vector<entry> dir;
    size_t got;                     // for PULL()
    for (;;) {
        // Read the local header.
        seq_t loc(LOCLEN);
        PULL(&strm, in, loc.data(), LOCLEN);
        if (got != LOCLEN || get4(loc.data()) != LOCSIG) {
            if (got >= 4 && get4(loc.data()) != CENSIG)
                std::cerr << "!! central signature did not follow locals\n";
            // This is the expected way to discover the end of the entries.
            ret = 0;
            break;
        }
        size_t nlen = get2(loc.data() + 26);
        size_t xlen = get2(loc.data() + 28);
        seq_t name(nlen);
        seq_t extra(xlen);
        PULL(&strm, in, name.data(), nlen);
        if (got != nlen) {
            // Read error or local header ended prematurely.
            ret = -5;
            break;
        }
        PULL(&strm, in, extra.data(), xlen);
        if (got != xlen) {
            // Read error or local header ended prematurely.
            ret = -5;
            break;
        }

        // Check that we can handle this entry.
        unsigned flags = get2(loc.data() + 6);
        if (flags & 0x2041) {
            // Can't handle encryption.
            ret = -3;
            break;
        }
        if ((flags & 8) && get2(loc.data() + 8) != 8) {
            // Can only handle a data descriptor if the method is deflate.
            ret = -4;
            break;
        }

        // Make the version needed to extract at least 4.5, in case Zip64
        // format extensions are used. Also we drop the high byte in case an OS
        // was put there. (Should not have been, but you never know.)
        put2(loc.data() + 4, std::max(loc[4], (uint8_t)45));

        // Get the CRC-32 and lengths.
        uint32_t crc = get4(loc.data() + 14);
        uint64_t clen = get4(loc.data() + 18);
        uint64_t ulen = get4(loc.data() + 22);
        auto at = extra_tag(1, extra);
        if (at <= -2 || ((ulen == MAX32 || clen == MAX32) && at == -1)) {
            // Invalid extra field structure, or missing the needed Zip64
            // extended information record.
            ret = -5;
            break;
        }
        if (at >= 0) {
            // Update ulen and/or clen from the extended information.
            size_t size = get2(extra.data() + at + 2);
            size_t i = at + 4;
            if (ulen == MAX32) {
                if (size < 8) {
                    // Invalid extended information structure.
                    ret = -5;
                    break;
                }
                ulen = get8(extra.data() + i);
                i += 8;
            }
            if (clen == MAX32) {
                if (size < i + 4 - at) {
                    // Invalid extended information structure.
                    ret = -5;
                    break;
                }
                clen = get8(extra.data() + i);
            }
        }

        // Write the local header.
        off = pos;
        WRITE(out, loc.data(), loc.size());
        WRITE(out, name.data(), name.size());
        WRITE(out, extra.data(), extra.size());
        pos += loc.size() + name.size() + extra.size();

        // Copy the compressed data and data descriptor.
        if (flags & 8) {
            // There will be a data descriptor. We need to decompress the data
            // to see where it ends. The compression method is deflate.
            // Decompress the deflate data and copy it to the output as we go.
            // Compute the compressed length, uncompressed length, and the
            // CRC-32 of the uncompressed data, in order to determine the
            // length of the data descriptor.
            crc = crc32(0, Z_NULL, 0);
            clen = ulen = 0;
            size_t had = strm.avail_in;
            uint8_t* beg = strm.next_in;
            inflateReset(&strm);
            do {
                if (strm.avail_in == 0) {
                    if (had) {
                        WRITE(out, beg, had);
                        clen += had;
                    }
                    READ(in, comp, sizeof(comp));
                    strm.avail_in = had = got;
                    strm.next_in = beg = (uint8_t*)comp;
                }
                uint8_t* data[16384];
                strm.avail_out = sizeof(data);
                strm.next_out = (uint8_t*)data;
                ret = inflate(&strm, Z_NO_FLUSH);
                unsigned net = sizeof(data) - strm.avail_out;
                crc = crc32(crc, (uint8_t*)data, net);
                ulen += net;
            } while (ret == Z_OK);
            if (ret != Z_STREAM_END) {
                // Invalid or incomplete deflate data, or write error.
                if (ret != -1)
                    // Bad deflate data.
                    ret = -6;
                break;
            }
            if (strm.avail_in != had) {
                WRITE(out, beg, had - strm.avail_in);
                clen += had - strm.avail_in;
            }
            pos += clen;

            // Verify and move past the data descriptor. Check for all four
            // possibilities. Set dlen to the length of the data descriptor. We
            // load four bytes past the longest possible descriptor, in case we
            // need to look for a header signature there.
            uint8_t desc[28];
            size_t dlen = 0;
            PULL(&strm, in, desc, sizeof(desc));
            if (got >= 24 && get4(desc) == DATSIG && get4(desc + 4) == crc &&
                    get8(desc + 8) == clen && get8(desc + 16) == ulen)
                dlen = 24;
            else if (got >= 20 && get4(desc) == crc &&
                        get8(desc + 4) == clen && get8(desc + 12) == ulen)
                dlen = 20;
            else if ((clen >> 32) == 0 && (ulen >> 32) == 0) {
                // 32-bit lengths are permitted.
                if (got >= 16 && get4(desc) == DATSIG &&
                        get4(desc + 4) == crc &&
                        get4(desc + 8) == clen && get4(desc + 12) == ulen)
                    dlen = 16;
                else if (got >= 12 && get4(desc) == crc &&
                            get4(desc + 4) == clen && get4(desc + 8) == ulen)
                    dlen = 12;
            }

            // Determine the number of bytes to eat from the input to get to
            // the next zip signature. If we found a good data descriptor, then
            // that's dlen.
            size_t eat = dlen;
            if (dlen == 0) {
                // No valid data descriptor matches the compressed data. Since
                // we don't know the length of that attempted data descriptor,
                // try to find where the next header starts.
                size_t i = got & ~3;
                while ((i -= 4) != 0)
                    if (get4(desc + i) == LOCSIG || get4(desc + i) == CENSIG)
                        break;
                eat = i;
            }

            if (eat < got) {
                // Return unused data descriptor bytes to the input buffer.
                if (strm.avail_in == 0) {
                    // Put the unused bytes in the buffer for the next pull.
                    memcpy(comp, desc + eat, got - eat);
                    strm.avail_in = got - eat;
                    strm.next_in = (uint8_t*)comp;
                }
                else {
                    // It all came from the buffer, so it's still there.
                    strm.avail_in += got - eat;
                    strm.next_in -= got - eat;
                }
            }

            if (dlen == 0) {
                // Build a new, correct data descriptor for this entry.
                dlen = (clen >> 32) || (ulen >> 32) || (off >> 32) ? 24 : 16;
                put4(desc, DATSIG);
                put4(desc + 4, crc);
                if (dlen == 16) {
                    put4(desc + 8, clen);
                    put4(desc + 12, ulen);
                }
                else {
                    put8(desc + 8, clen);
                    put8(desc + 16, ulen);
                }
            }

            // Write the data descriptor.
            WRITE(out, desc, dlen);
            pos += dlen;
        }
        else {
            // No data descriptor. Simply copy clen bytes. This will work for
            // any compression method.
            uint64_t copy = clen;
            if (strm.avail_in && copy) {
                got = std::min(copy, (uint64_t)strm.avail_in);
                WRITE(out, strm.next_in, got);
                strm.avail_in -= got;
                strm.next_in += got;
                pos += got;
                copy -= got;
            }
            while (copy) {
                size_t get = std::min(copy, (uint64_t)sizeof(comp));
                READ(in, comp, get);
                if (got == 0) {
                    // Premature end of compressed data.
                    ret = -6;
                    break;
                }
                WRITE(out, comp, got);
                pos += got;
                copy -= got;
            }
            if (copy)
                // Read error or premature end (ret set properly).
                break;
        }

        // Good local entry. Add the local header information to the directory.
        dir.emplace_back(loc, name, extra, crc, clen, ulen, off);
    }
    inflateEnd(&strm);

    // Even if we arrive here with an error, we will finish off the zip file
    // with a directory and end records for the local entries written so far.
    if (ret == -6) {
        // Try to back up over the bad entry. This will work if out is a file,
        // including if it's stdout redirected to a file. If however out is a
        // pipe, this will do nothing. That's ok too. In that case, a partial
        // local entry was written, but its information was not added to dir,
        // so it will not appear in the central directory. The resulting zip
        // file will still be extractable, but will have some wasted space
        // between the last local entry and the central directory.
        if (ftruncate(out, off) != 1 && lseek(out, off, SEEK_SET) != -1)
            pos = off;
    }

    // Write the central records.
    unsigned maxver = 45;   // maximum version needed to extract
    auto first = pos;       // offset of the first central directory record
    for (auto& ent : dir) {
        // Snip off extra fields as appropriate and delete the Zip64 extended
        // information field, if there. We'll make a new extended information
        // field if needed.
        extra_snip(ent.extra);

        // Make a Zip64 extended information record. If it's not empty,
        // prepend to to the existing extra data.
        seq_t zip64(4);
        if (ent.ulen >= MAX32) {
            zip64.resize(12);
            put8(zip64.data() + 4, ent.ulen);
        }
        if (ent.clen >= MAX32) {
            size_t have = zip64.size();
            zip64.resize(have + 8);
            put8(zip64.data() + have, ent.clen);
        }
        if (ent.off >= MAX32) {
            size_t have = zip64.size();
            zip64.resize(have + 8);
            put8(zip64.data() + have, ent.off);
        }
        if (zip64.size() > 4) {
            // A Zip64 extra data field is needed.
            put2(zip64.data(), 1);
            put2(zip64.data() + 2, zip64.size() - 4);
            ent.extra.insert(ent.extra.begin(), zip64.begin(), zip64.end());
        }

        // Update the maximum version needed to extract for the Zip64 end of
        // central directory record.
        maxver = std::max(maxver, (unsigned)ent.loc[4]);

        // Prepare the central record. The OS is set to MS-DOS and the external
        // file attributes are set to zero. (It is not necessary to indicate a
        // directory entry as such in the external attributes in order to be
        // extracted correctly. If the name has a trailing delimiter, then the
        // extractor takes it to be a directory.)
        seq_t cen(CENLEN);
        put4(cen.data(), CENSIG);
        memcpy(cen.data() + 4, ent.loc.data() + 4, 2);
        memcpy(cen.data() + 6, ent.loc.data() + 4, 10);
        put4(cen.data() + 16, ent.crc);
        put4(cen.data() + 20, ent.clen >= MAX32 ? MAX32 : ent.clen);
        put4(cen.data() + 24, ent.ulen >= MAX32 ? MAX32 : ent.ulen);
        put2(cen.data() + 28, ent.name.size());
        put2(cen.data() + 30, ent.extra.size());
        put4(cen.data() + 42, ent.off >= MAX32 ? MAX32 : ent.off);

        // Write the central record, name, and extra field.
        WRITE(out, cen.data(), cen.size());
        WRITE(out, ent.name.data(), ent.name.size());
        WRITE(out, ent.extra.data(), ent.extra.size());
        pos += cen.size() + ent.name.size() + ent.extra.size();
    }

    // Write the end records.
    uint64_t len = pos - first;
    if (dir.size() >= MAX16 || len >= MAX32 || first >= MAX32) {
        // Zip64 end of central directory record.
        seq_t xend(Z64ENDLEN);
        put4(xend.data(), Z64ENDSIG);       // Zip64 end record signature
        put8(xend.data() + 4, Z64ENDLEN - 12);  // length of remaining record
        put2(xend.data() + 12, maxver);     // version made by
        put2(xend.data() + 14, maxver);     // version needed to extract
        put8(xend.data() + 24, dir.size()); // number of entries here
        put8(xend.data() + 32, dir.size()); // total number of entries
        put8(xend.data() + 40, len);        // length of central directory
        put8(xend.data() + 48, first);      // offset of central directory

        // Zip64 end of central directory locator.
        seq_t xloc(Z64LOCLEN);
        put4(xloc.data(), Z64LOCSIG);       // Zip64 end locator signature
        put8(xloc.data() + 8, pos);         // offset of Zip64 end record
        put4(xloc.data() + 16, 1);          // total number of disks

        // Write the Zip64 records.
        WRITE(out, xend.data(), xend.size());
        WRITE(out, xloc.data(), xloc.size());
    }

    // End of central directory record.
    seq_t end(ENDLEN);
    put4(end.data(), ENDSIG);               // end record signature
    put2(end.data() + 8,                    // number of entries on this disk
         dir.size() >= MAX16 ? MAX16 : dir.size());
    put2(end.data() + 10,                   // total number of entries
         dir.size() >= MAX16 ? MAX16 : dir.size());
    put4(end.data() + 12,                   // central directory length
         len >= MAX32 ? MAX32 : len);
    put4(end.data() + 16,                   // central directory offset
         first >= MAX32 ? MAX32 : first);

    // Write the end record.
    WRITE(out, end.data(), end.size());

    // The zip file is complete. If ret is not zero, then not all of the local
    // entries from the input were included in the output.
    return ret;
}

// --- main ---

// Rebuild the zip file on stdin, writing the result to stdout.
int main() {
    BINARY(0);  BINARY(1);
    int ret = zip_tail(0, 1);
    if (ret)
        std::cerr << "** ziptail error: " << errmsg[-1 - ret] << '\n';
    if (!std::cout.good())
        std::cerr << "** write error\n";
    return ret != 0;
}
© www.soinside.com 2019 - 2024. All rights reserved.