ZipDosTimeDstTest.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.tika.parser.pkg;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.TimeZone;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Isolated;
import org.junit.jupiter.api.parallel.ResourceLock;
import org.junit.jupiter.api.parallel.Resources;

import org.apache.tika.TikaTest;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.FileSystem;
import org.apache.tika.metadata.Metadata;

/**
 * A DOS time inside the host's DST spring-forward gap: commons-compress resolves it in the JVM
 * zone and moves 02:30 to 03:30; the zip path reads the raw header field instead.
 */
@Isolated
@ResourceLock(Resources.TIME_ZONE)
public class ZipDosTimeDstTest extends TikaTest {

    private static TimeZone saved;

    @BeforeAll
    public static void berlin() {
        saved = TimeZone.getDefault();
        TimeZone.setDefault(TimeZone.getTimeZone("Europe/Berlin"));
    }

    @AfterAll
    public static void restore() {
        TimeZone.setDefault(saved);
    }

    @Test
    public void testDosTimeInDstGap() throws Exception {
        List<Metadata> list = getRecursiveMetadata(TikaInputStream.get(zipAt(2021, 3, 28, 2, 30, 10)),
                new ZipParser(), false);
        assertEquals("2021-03-28T02:30:10", list.get(1).get(FileSystem.MODIFIED));
    }

    @Test
    public void testDosTimeOutsideGap() throws Exception {
        List<Metadata> list = getRecursiveMetadata(TikaInputStream.get(zipAt(2021, 7, 1, 9, 15, 0)),
                new ZipParser(), false);
        assertEquals("2021-07-01T09:15:00", list.get(1).get(FileSystem.MODIFIED));
    }

    // java.util.zip writes the DOS fields; patch both headers with the exact wall clock
    private static byte[] zipAt(int year, int month, int day, int hour, int min, int sec) throws Exception {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try (ZipOutputStream zos = new ZipOutputStream(bos)) {
            zos.putNextEntry(new ZipEntry("a.txt"));
            zos.write("hello".getBytes(StandardCharsets.US_ASCII));
            zos.closeEntry();
        }
        byte[] b = bos.toByteArray();
        int time = (hour << 11) | (min << 5) | (sec / 2);
        int date = ((year - 1980) << 9) | (month << 5) | day;
        patch(b, 10, time, date);
        for (int i = 0; i + 4 < b.length; i++) {
            if (b[i] == 0x50 && b[i + 1] == 0x4b && b[i + 2] == 1 && b[i + 3] == 2) {
                patch(b, i + 12, time, date);
            }
        }
        return b;
    }

    private static void patch(byte[] b, int off, int time, int date) {
        b[off] = (byte) time;
        b[off + 1] = (byte) (time >> 8);
        b[off + 2] = (byte) date;
        b[off + 3] = (byte) (date >> 8);
    }
}