Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,16 @@
import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
import org.apache.jackrabbit.webdav.xml.Namespace;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.time.temporal.ChronoField;
import java.util.Date;
import java.util.Locale;

Expand All @@ -34,40 +42,117 @@

@SuppressFBWarnings("FS")
public class WebdavUtils {
private static final SimpleDateFormat DATETIME_FORMATS[] = {
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US),
new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US),
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.sss'Z'", Locale.US),
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US),
new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US),
new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US),
new SimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", Locale.US),
new SimpleDateFormat("yyyy-MM-dd hh:mm:ss", Locale.US)
};
private static final DateTimeFormatter HTTP_DATE_FORMATTER =
DateTimeFormatter
.ofPattern("EEE, dd MMM uuuu HH:mm:ss zzz", Locale.US)
.withResolverStyle(ResolverStyle.STRICT);

private static final DateTimeFormatter RFC_850_DATE_FORMATTER =
new DateTimeFormatterBuilder()
.parseCaseSensitive()
.appendPattern("EEEE, dd-MMM-")
.appendValueReduced(ChronoField.YEAR, 2, 2, 2000)
.appendPattern(" HH:mm:ss zzz")
.toFormatter(Locale.US)
.withResolverStyle(ResolverStyle.STRICT);

private static final DateTimeFormatter ASCTIME_DATE_FORMATTER =
DateTimeFormatter
.ofPattern("EEE MMM ppd HH:mm:ss uuuu", Locale.US)
.withResolverStyle(ResolverStyle.STRICT);

private static final DateTimeFormatter SHARE_EXPIRATION_FORMATTER =
DateTimeFormatter
.ofPattern("uuuu-MM-dd HH:mm:ss", Locale.US)
.withResolverStyle(ResolverStyle.STRICT);

/**
* Parses an HTTP/WebDAV date in IMF-fixdate format (99% of modern traffic).
*
* For full RFC complianace, also supports obsolete RFC 850 and ANSI C asctime()
* formats.
*
* @param date HTTP/WebDAV date
* @return parsed date, or {@code null} if invalid
*/
public static @Nullable
Date parseResponseDate(String date) {
Date returnDate;
SimpleDateFormat format;
for (int i = 0; i < DATETIME_FORMATS.length; ++i) {
try {
format = DATETIME_FORMATS[i];
synchronized (format) {
returnDate = format.parse(date);
}
return returnDate;
} catch (ParseException e) {
// this is not the format
if (date == null || date.isEmpty()) {
return null;
}

try {
ZonedDateTime parsed =
ZonedDateTime.parse(date, HTTP_DATE_FORMATTER);

return Date.from(parsed.toInstant());
} catch (DateTimeParseException e) {
// Try RFC 850 below.
}

try {
ZonedDateTime parsed =
ZonedDateTime.parse(date, RFC_850_DATE_FORMATTER);

// Native date arithmetic protects the 50-year spec calculation from leap day drift
Instant fiftyYearsFromNow =
ZonedDateTime.now(ZoneOffset.UTC)
.plusYears(50)
.toInstant();

if (parsed.toInstant().isAfter(fiftyYearsFromNow)) {
parsed = parsed.minusYears(100);
}

return Date.from(parsed.toInstant());
} catch (DateTimeParseException e) {
// Try ANSI C asctime() below.
}

try {
LocalDateTime parsed =
LocalDateTime.parse(date, ASCTIME_DATE_FORMATTER);

return Date.from(
parsed.atOffset(ZoneOffset.UTC).toInstant()
);
} catch (DateTimeParseException e) {
return null;
}
return null;
}

/**
* Encodes a path according to URI RFC 2396.
*
* Parses a Share API expiration date.
*
* <p>The value has no timezone suffix and is therefore interpreted using
* the device default timezone.</p>
*
* @param date expiration date returned by the Share API
* @return parsed date, or {@code null} if invalid
*/
public static @Nullable
Date parseShareExpirationDate(String date) {
if (date == null || date.isEmpty()) {
return null;
}

try {
LocalDateTime parsed =
LocalDateTime.parse(date, SHARE_EXPIRATION_FORMATTER);

return Date.from(
parsed.atZone(ZoneId.systemDefault()).toInstant()
);
} catch (DateTimeParseException e) {
return null;
}
}

/**
* Encodes a path according to URI RFC 2396.
*
* If the received path doesn't start with "/", the method adds it.
*
*
* @param remoteFilePath Path
* @return Encoded path according to RFC 2396, always starting with "/"
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ private void readElement(XmlPullParser parser, ArrayList<OCShare> shares)
case NODE_EXPIRATION:
String expirationValue = readNode(parser, NODE_EXPIRATION);
if (expirationValue.length() > 0) {
Date date = WebdavUtils.parseResponseDate(expirationValue);
Date date = WebdavUtils.parseShareExpirationDate(expirationValue);
if (date != null) {
share.setExpirationDate(date.getTime());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* Nextcloud Android Library
*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: MIT
*/
package com.owncloud.android.lib.common.network;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;

import org.junit.Test;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;

public class WebdavUtilsTest {

@Test
public void parseResponseDate_parsesImfFixdate() {
Date result = WebdavUtils.parseResponseDate(
"Wed, 09 Sep 2026 08:49:37 GMT"
);

assertNotNull(result);
assertEquals(
Instant.parse("2026-09-09T08:49:37Z"),
result.toInstant()
);
}

@Test
public void parseResponseDate_parsesRfc850Date() {
Date result = WebdavUtils.parseResponseDate(
"Sunday, 06-Nov-94 08:49:37 GMT"
);

assertNotNull(result);
assertEquals(
Instant.parse("1994-11-06T08:49:37Z"),
result.toInstant()
);
}

@Test
public void parseResponseDate_parsesAsctimeDateWithSingleDigitDay() {
Date result = WebdavUtils.parseResponseDate(
"Sun Nov 6 08:49:37 1994"
);

assertNotNull(result);
assertEquals(
Instant.parse("1994-11-06T08:49:37Z"),
result.toInstant()
);
}

@Test
public void parseResponseDate_parsesAsctimeDateWithTwoDigitDay() {
Date result = WebdavUtils.parseResponseDate(
"Sun Nov 16 08:49:37 1994"
);

assertNotNull(result);
assertEquals(
Instant.parse("1994-11-16T08:49:37Z"),
result.toInstant()
);
}

@Test
public void parseResponseDate_rejectsInvalidWeekday() {
Date result = WebdavUtils.parseResponseDate(
"Monday, 06-Nov-94 08:49:37 GMT"
);

assertNull(result);
}

@Test
public void parseResponseDate_rejectsInvalidDate() {
Date result = WebdavUtils.parseResponseDate(
"Wed, 31 Feb 2026 08:49:37 GMT"
);

assertNull(result);
}

@Test
public void parseResponseDate_rejectsTrailingCharacters() {
Date result = WebdavUtils.parseResponseDate(
"Wed, 09 Sep 2026 08:49:37 GMT trailing"
);

assertNull(result);
}

@Test
public void parseResponseDate_returnsNullForNullAndEmptyInput() {
assertNull(WebdavUtils.parseResponseDate(null));
assertNull(WebdavUtils.parseResponseDate(""));
}

@Test
public void parseShareExpirationDate_parsesShareApiFormat() {
String value = "2026-09-10 23:59:59";

Date result = WebdavUtils.parseShareExpirationDate(value);

assertNotNull(result);
assertEquals(
LocalDateTime.parse(value)
.atZone(ZoneId.systemDefault())
.toInstant(),
result.toInstant()
);
}

@Test
public void parseShareExpirationDate_parses24HourTime() {
String value = "2026-09-10 23:00:00";

Date result = WebdavUtils.parseShareExpirationDate(value);

assertNotNull(result);
assertEquals(
LocalDateTime.parse(value)
.atZone(ZoneId.systemDefault())
.toInstant(),
result.toInstant()
);
}

@Test
public void parseShareExpirationDate_rejectsInvalidDate() {
Date result = WebdavUtils.parseShareExpirationDate(
"2026-02-29 12:00:00"
);

assertNull(result);
}

@Test
public void parseShareExpirationDate_rejectsHttpDate() {
Date result = WebdavUtils.parseShareExpirationDate(
"Wed, 09 Sep 2026 08:49:37 GMT"
);

assertNull(result);
}

@Test
public void parseShareExpirationDate_returnsNullForNullAndEmptyInput() {
assertNull(WebdavUtils.parseShareExpirationDate(null));
assertNull(WebdavUtils.parseShareExpirationDate(""));
}
}
Loading