Page MenuHomePhabricator

D16521.diff
No OneTemporary

D16521.diff

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -137,6 +137,7 @@
'PhutilCalendarEventNode' => 'parser/calendar/data/PhutilCalendarEventNode.php',
'PhutilCalendarNode' => 'parser/calendar/data/PhutilCalendarNode.php',
'PhutilCalendarRawNode' => 'parser/calendar/data/PhutilCalendarRawNode.php',
+ 'PhutilCalendarRootNode' => 'parser/calendar/data/PhutilCalendarRootNode.php',
'PhutilCallbackFilterIterator' => 'utils/PhutilCallbackFilterIterator.php',
'PhutilCallbackSignalHandler' => 'future/exec/PhutilCallbackSignalHandler.php',
'PhutilChannel' => 'channel/PhutilChannel.php',
@@ -228,6 +229,7 @@
'PhutilHgsprintfTestCase' => 'xsprintf/__tests__/PhutilHgsprintfTestCase.php',
'PhutilHighIntensityIntervalDaemon' => 'daemon/torture/PhutilHighIntensityIntervalDaemon.php',
'PhutilICSParser' => 'parser/calendar/ics/PhutilICSParser.php',
+ 'PhutilICSParserException' => 'parser/calendar/ics/PhutilICSParserException.php',
'PhutilICSParserTestCase' => 'parser/calendar/ics/__tests__/PhutilICSParserTestCase.php',
'PhutilINIParserException' => 'parser/exception/PhutilINIParserException.php',
'PhutilIPAddress' => 'ip/PhutilIPAddress.php',
@@ -710,7 +712,8 @@
'PhutilCalendarDocumentNode' => 'PhutilCalendarContainerNode',
'PhutilCalendarEventNode' => 'PhutilCalendarNode',
'PhutilCalendarNode' => 'Phobject',
- 'PhutilCalendarRawNode' => 'PhutilCalendarNode',
+ 'PhutilCalendarRawNode' => 'PhutilCalendarContainerNode',
+ 'PhutilCalendarRootNode' => 'PhutilCalendarContainerNode',
'PhutilCallbackFilterIterator' => 'FilterIterator',
'PhutilCallbackSignalHandler' => 'PhutilSignalHandler',
'PhutilChannel' => 'Phobject',
@@ -808,6 +811,7 @@
'PhutilHgsprintfTestCase' => 'PhutilTestCase',
'PhutilHighIntensityIntervalDaemon' => 'PhutilTortureTestDaemon',
'PhutilICSParser' => 'Phobject',
+ 'PhutilICSParserException' => 'Exception',
'PhutilICSParserTestCase' => 'PhutilTestCase',
'PhutilINIParserException' => 'Exception',
'PhutilIPAddress' => 'Phobject',
diff --git a/src/parser/calendar/data/PhutilCalendarRawNode.php b/src/parser/calendar/data/PhutilCalendarRawNode.php
--- a/src/parser/calendar/data/PhutilCalendarRawNode.php
+++ b/src/parser/calendar/data/PhutilCalendarRawNode.php
@@ -1,7 +1,7 @@
<?php
final class PhutilCalendarRawNode
- extends PhutilCalendarNode {
+ extends PhutilCalendarContainerNode {
const NODETYPE = 'raw';
diff --git a/src/parser/calendar/data/PhutilCalendarRootNode.php b/src/parser/calendar/data/PhutilCalendarRootNode.php
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/data/PhutilCalendarRootNode.php
@@ -0,0 +1,12 @@
+<?php
+
+final class PhutilCalendarRootNode
+ extends PhutilCalendarContainerNode {
+
+ const NODETYPE = 'root';
+
+ public function getDocuments() {
+ return $this->getChildrenOfType(PhutilCalendarDocumentNode::NODETYPE);
+ }
+
+}
diff --git a/src/parser/calendar/ics/PhutilICSParser.php b/src/parser/calendar/ics/PhutilICSParser.php
--- a/src/parser/calendar/ics/PhutilICSParser.php
+++ b/src/parser/calendar/ics/PhutilICSParser.php
@@ -5,42 +5,67 @@
private $stack;
private $node;
private $document;
+ private $lines;
+ private $cursor;
+
+ const PARSE_MISSING_END = 'missing-end';
+ const PARSE_INITIAL_UNFOLD = 'initial-unfold';
+ const PARSE_UNEXPECTED_CHILD = 'unexpected-child';
+ const PARSE_EXTRA_END = 'extra-end';
+ const PARSE_MISMATCHED_SECTIONS = 'mismatched-sections';
+ const PARSE_ROOT_PROPERTY = 'root-property';
+ const PARSE_BAD_BASE64 = 'bad-base64';
+ const PARSE_BAD_BOOLEAN = 'bad-boolean';
+ const PARSE_UNEXPECTED_TEXT = 'unexpected-text';
+ const PARSE_MALFORMED_DOUBLE_QUOTE = 'malformed-double-quote';
+ const PARSE_MALFORMED_PARAMETER_NAME = 'malformed-parameter';
+ const PARSE_MALFORMED_PROPERTY = 'malformed-property';
+ const PARSE_MISSING_VALUE = 'missing-value';
+ const PARSE_UNESCAPED_BACKSLASH = 'unescaped-backslash';
public function parseICSData($data) {
$this->stack = array();
$this->node = null;
- $this->document = null;
+ $this->cursor = null;
$lines = $this->unfoldICSLines($data);
+ $this->lines = $lines;
- foreach ($lines as $line) {
+ $root = $this->newICSNode('<ROOT>');
+ $this->stack[] = $root;
+ $this->node = $root;
+
+ foreach ($lines as $key => $line) {
+ $this->cursor = $key;
$matches = null;
if (preg_match('(^BEGIN:(.*)\z)', $line, $matches)) {
$this->beginParsingNode($matches[1]);
} else if (preg_match('(^END:(.*)\z)', $line, $matches)) {
$this->endParsingNode($matches[1]);
} else {
+ if (count($this->stack) < 2) {
+ $this->raiseParseFailure(
+ self::PARSE_ROOT_PROPERTY,
+ pht(
+ 'Found unexpected property at ICS document root.'));
+ }
$this->parseICSProperty($line);
}
}
- if (!$this->document) {
- $this->raiseParseFailure(
- pht(
- 'Expected ICS document to define a "VCALENDAR" section.'));
- }
-
- if ($this->stack) {
+ if (count($this->stack) > 1) {
$this->raiseParseFailure(
+ self::PARSE_MISSING_END,
pht(
'Expected all "BEGIN:" sections in ICS document to have '.
'corresponding "END:" sections.'));
}
- $document = $this->document;
- $this->document = null;
+ $this->node = null;
+ $this->lines = null;
+ $this->cursor = null;
- return $document;
+ return $root;
}
private function getNode() {
@@ -49,22 +74,25 @@
private function unfoldICSLines($data) {
$lines = phutil_split_lines($data, $retain_endings = false);
+ $this->lines = $lines;
// ICS files are wrapped at 75 characters, with overlong lines continued
// on the following line with an initial space or tab. Unwrap all of the
// lines in the file.
$last = null;
foreach ($lines as $idx => $line) {
+ $this->cursor = $idx;
if (!preg_match('/^[ \t]/', $line)) {
$last = $idx;
continue;
}
if ($last === null) {
- throw new Exception(
+ $this->raiseParseFailure(
+ self::PARSE_INITIAL_UNFOLD,
pht(
'First line of ICS file begins with a space or tab, but this '.
- 'marks a continuation line.'));
+ 'marks a line which should be unfolded.'));
}
$lines[$last] = $lines[$last].substr($line, 1);
@@ -78,31 +106,15 @@
$node = $this->getNode();
$new_node = $this->newICSNode($type);
- if ($node) {
- if ($node instanceof PhutilCalendarContainerNode) {
- $node->appendChild($new_node);
- } else {
- $this->raiseParseFailure(
- pht(
- 'Found unexpected node "%s" inside node "%s".',
- $new_node->getAttribute('ics.type'),
- $node->getAttribute('ics.type')));
- }
+ if ($node instanceof PhutilCalendarContainerNode) {
+ $node->appendChild($new_node);
} else {
- if ($new_node instanceof PhutilCalendarDocumentNode) {
- if ($this->document) {
- $this->raiseParseFailure(
- pht(
- 'Found multiple "VCALENDAR" nodes in ICS document, '.
- 'expected only one.'));
- } else {
- $this->document = $new_node;
- }
- } else {
- $this->raiseParseFailure(
- pht(
- 'Expected ICS document to begin "BEGIN:VCALENDAR".'));
- }
+ $this->raiseParseFailure(
+ self::PARSE_UNEXPECTED_CHILD,
+ pht(
+ 'Found unexpected node "%s" inside node "%s".',
+ $new_node->getAttribute('ics.type'),
+ $node->getAttribute('ics.type')));
}
$this->stack[] = $new_node;
@@ -113,6 +125,9 @@
private function newICSNode($type) {
switch ($type) {
+ case '<ROOT>':
+ $node = new PhutilCalendarRootNode();
+ break;
case 'VCALENDAR':
$node = new PhutilCalendarDocumentNode();
break;
@@ -131,8 +146,9 @@
private function endParsingNode($type) {
$node = $this->getNode();
- if (!$node) {
+ if ($node instanceof PhutilCalendarRootNode) {
$this->raiseParseFailure(
+ self::PARSE_EXTRA_END,
pht(
'Found unexpected "END" without a "BEGIN".'));
}
@@ -140,6 +156,7 @@
$old_type = $node->getAttribute('ics.type');
if ($old_type != $type) {
$this->raiseParseFailure(
+ self::PARSE_MISMATCHED_SECTIONS,
pht(
'Found mismatched "BEGIN" ("%s") and "END" ("%s") sections.',
$old_type,
@@ -147,11 +164,7 @@
}
array_pop($this->stack);
- if ($this->stack) {
- $this->node = last($this->stack);
- } else {
- $this->node = null;
- }
+ $this->node = last($this->stack);
return $this;
}
@@ -163,12 +176,12 @@
// by either a ";" (to begin a list of parameters) or a ":" (to begin
// the actual field body).
- $ok = preg_match('(^([^;:]+)([;:])(.*)\z)', $line, $matches);
+ $ok = preg_match('(^([A-Za-z0-9-]+)([;:])(.*)\z)', $line, $matches);
if (!$ok) {
$this->raiseParseFailure(
+ self::PARSE_MALFORMED_PROPERTY,
pht(
- 'Found malformed line in ICS document: %s',
- $line));
+ 'Found malformed property in ICS document.'));
}
$name = $matches[1];
@@ -185,6 +198,7 @@
$ok = preg_match('(^([^=]+)=)', $body, $matches);
if (!$ok) {
$this->raiseParseFailure(
+ self::PARSE_MALFORMED_PARAMETER_NAME,
pht(
'Found malformed property in ICS document: %s',
$body));
@@ -206,24 +220,17 @@
$matches);
if (!$ok) {
$this->raiseParseFailure(
+ self::PARSE_MALFORMED_DOUBLE_QUOTE,
pht(
'Found malformed double-quoted string in ICS document '.
- 'parameter value: %s',
- $body));
+ 'parameter value.'));
}
} else {
$is_quoted = false;
- $ok = preg_match(
- '(^([^\x00-\x08\x10-\x19";:,]*))',
- $body,
- $matches);
- if (!$ok) {
- $this->raiseParseFailure(
- pht(
- 'Found malformed unquoted string in ICS document '.
- 'parameter value: %s',
- $body));
- }
+
+ // It's impossible for this not to match since it can match
+ // nothing, and it's valid for it to match nothing.
+ preg_match('(^([^\x00-\x08\x10-\x19";:,]*))', $body, $matches);
}
// NOTE: RFC5545 says "Property parameter values that are not in
@@ -239,6 +246,7 @@
$body = substr($body, strlen($matches[0]));
if (!strlen($body)) {
$this->raiseParseFailure(
+ self::PARSE_MISSING_VALUE,
pht(
'Expected ":" after parameters in ICS document property.'));
}
@@ -250,17 +258,27 @@
continue;
}
+ // If we have a semicolon, we're going to read another parameter.
+ if ($body[0] == ';') {
+ break;
+ }
+
// If we have a colon, this is the last value and also the last
// property. Break, then handle the colon below.
if ($body[0] == ':') {
break;
}
+ $short_body = id(new PhutilUTF8StringTruncator())
+ ->setMaximumGlyphs(32)
+ ->truncateString($body);
+
// We aren't expecting anything else.
$this->raiseParseFailure(
+ self::PARSE_UNEXPECTED_TEXT,
pht(
- 'Found unexpected text after reading parameter value: %s',
- $body));
+ 'Found unexpected text ("%s") after reading parameter value.',
+ $short_body));
}
$parameters[] = array(
@@ -268,6 +286,11 @@
'values' => $param_values,
);
+ if ($body[0] == ';') {
+ $body = substr($body, 1);
+ continue;
+ }
+
if ($body[0] == ':') {
$body = substr($body, 1);
break;
@@ -278,6 +301,7 @@
$value = $this->unescapeFieldValue($name, $parameters, $body);
$node = $this->getNode();
+
$raw = $node->getAttribute('ics.properties', array());
$raw[] = array(
'name' => $name,
@@ -409,9 +433,10 @@
switch ($value_type) {
case 'BINARY':
- $result = base64_decode($data);
+ $result = base64_decode($data, true);
if ($result === false) {
$this->raiseParseFailure(
+ self::PARSE_BAD_BASE64,
pht(
'Unable to decode base64 data: %s',
$data));
@@ -425,6 +450,7 @@
$result = phutil_utf8_strtolower($data);
if (!isset($map[$result])) {
$this->raiseParseFailure(
+ self::PARSE_BAD_BOOLEAN,
pht(
'Unexpected BOOLEAN value "%s".',
$data));
@@ -517,7 +543,8 @@
}
if ($esc) {
- $this->raiseParsFailure(
+ $this->raiseParseFailure(
+ self::PARSE_UNESCAPED_BACKSLASH,
pht(
'ICS document contains TEXT value ending with unescaped '.
'backslash.'));
@@ -528,8 +555,21 @@
return $result;
}
- private function raiseParseFailure($message) {
- throw new Exception($message);
+ private function raiseParseFailure($code, $message) {
+ if ($this->lines && isset($this->lines[$this->cursor])) {
+ $message = pht(
+ "ICS Parse Error near line %s:\n\n>>> %s\n\n%s",
+ $this->cursor + 1,
+ $this->lines[$this->cursor],
+ $message);
+ } else {
+ $message = pht(
+ 'ICS Parse Error: %s',
+ $message);
+ }
+
+ throw id(new PhutilICSParserException($message))
+ ->setParserFailureCode($code);
}
}
diff --git a/src/parser/calendar/ics/PhutilICSParserException.php b/src/parser/calendar/ics/PhutilICSParserException.php
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/PhutilICSParserException.php
@@ -0,0 +1,16 @@
+<?php
+
+final class PhutilICSParserException extends Exception {
+
+ private $parserFailureCode;
+
+ public function setParserFailureCode($code) {
+ $this->parserFailureCode = $code;
+ return $this;
+ }
+
+ public function getParserFailureCode() {
+ return $this->parserFailureCode;
+ }
+
+}
diff --git a/src/parser/calendar/ics/__tests__/PhutilICSParserTestCase.php b/src/parser/calendar/ics/__tests__/PhutilICSParserTestCase.php
--- a/src/parser/calendar/ics/__tests__/PhutilICSParserTestCase.php
+++ b/src/parser/calendar/ics/__tests__/PhutilICSParserTestCase.php
@@ -3,7 +3,11 @@
final class PhutilICSParserTestCase extends PhutilTestCase {
public function testICSParser() {
- $document = $this->parseICSDocument('simple.ics');
+ $root = $this->parseICSDocument('simple.ics');
+
+ $documents = $root->getDocuments();
+ $this->assertEqual(1, count($documents));
+ $document = head($documents);
$events = $document->getEvents();
$this->assertEqual(1, count($events));
@@ -69,6 +73,68 @@
$event->getAttribute('ics.properties'));
}
+ public function testICSParserErrors() {
+ $map = array(
+ 'err-missing-end.ics' => PhutilICSParser::PARSE_MISSING_END,
+ 'err-bad-base64.ics' => PhutilICSParser::PARSE_BAD_BASE64,
+ 'err-bad-boolean.ics' => PhutilICSParser::PARSE_BAD_BOOLEAN,
+ 'err-extra-end.ics' => PhutilICSParser::PARSE_EXTRA_END,
+ 'err-initial-unfold.ics' => PhutilICSParser::PARSE_INITIAL_UNFOLD,
+ 'err-malformed-double-quote.ics' =>
+ PhutilICSParser::PARSE_MALFORMED_DOUBLE_QUOTE,
+ 'err-malformed-parameter.ics' =>
+ PhutilICSParser::PARSE_MALFORMED_PARAMETER_NAME,
+ 'err-malformed-property.ics' =>
+ PhutilICSParser::PARSE_MALFORMED_PROPERTY,
+ 'err-missing-value.ics' => PhutilICSParser::PARSE_MISSING_VALUE,
+ 'err-mixmatched-sections.ics' =>
+ PhutilICSParser::PARSE_MISMATCHED_SECTIONS,
+ 'err-root-property.ics' => PhutilICSParser::PARSE_ROOT_PROPERTY,
+ 'err-unescaped-backslash.ics' =>
+ PhutilICSParser::PARSE_UNESCAPED_BACKSLASH,
+ 'err-unexpected-child.ics' => PhutilICSParser::PARSE_UNEXPECTED_CHILD,
+ 'err-unexpected-text.ics' => PhutilICSParser::PARSE_UNEXPECTED_TEXT,
+
+ 'simple.ics' => null,
+ 'good-boolean.ics' => null,
+ 'multiple-vcalendars.ics' => null,
+ );
+
+ foreach ($map as $test_file => $expect) {
+ $caught = null;
+ try {
+ $this->parseICSDocument($test_file);
+ } catch (PhutilICSParserException $ex) {
+ $caught = $ex;
+ }
+
+ if ($expect === null) {
+ $this->assertTrue(
+ ($caught === null),
+ pht(
+ 'Expected no exception parsing "%s", got: %s',
+ $test_file,
+ (string)$ex));
+ } else {
+ if ($caught) {
+ $code = $ex->getParserFailureCode();
+ $explain = pht(
+ 'Expected one exception parsing "%s", got a different '.
+ 'one: %s',
+ $test_file,
+ (string)$ex);
+ } else {
+ $code = null;
+ $explain = pht(
+ 'Expected exception parsing "%s", got none.',
+ $test_file);
+ }
+
+ $this->assertEqual($expect, $code, $explain);
+ }
+ }
+ }
+
private function parseICSDocument($name) {
$path = dirname(__FILE__).'/data/'.$name;
$data = Filesystem::readFile($path);
diff --git a/src/parser/calendar/ics/__tests__/data/err-bad-base64.ics b/src/parser/calendar/ics/__tests__/data/err-bad-base64.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/err-bad-base64.ics
@@ -0,0 +1,5 @@
+BEGIN:VCALENDAR
+BEGIN:VEVENT
+DATA;VALUE=BINARY;ENCODING=BASE64:<QUACK! QUACK!>
+END:VEVENT
+END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-bad-boolean.ics b/src/parser/calendar/ics/__tests__/data/err-bad-boolean.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/err-bad-boolean.ics
@@ -0,0 +1,5 @@
+BEGIN:VCALENDAR
+BEGIN:VEVENT
+DUCK;VALUE=BOOLEAN:QUACK
+END:VEVENT
+END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-extra-end.ics b/src/parser/calendar/ics/__tests__/data/err-extra-end.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/err-extra-end.ics
@@ -0,0 +1 @@
+END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-initial-unfold.ics b/src/parser/calendar/ics/__tests__/data/err-initial-unfold.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/err-initial-unfold.ics
@@ -0,0 +1,2 @@
+ BEGIN:VCALENDAR
+END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-malformed-double-quote.ics b/src/parser/calendar/ics/__tests__/data/err-malformed-double-quote.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/err-malformed-double-quote.ics
@@ -0,0 +1,5 @@
+BEGIN:VCALENDAR
+BEGIN:VEVENT
+A;B="C:D
+END:VEVENT
+END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-malformed-parameter.ics b/src/parser/calendar/ics/__tests__/data/err-malformed-parameter.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/err-malformed-parameter.ics
@@ -0,0 +1,5 @@
+BEGIN:VCALENDAR
+BEGIN:VEVENT
+A;B:C
+END:VEVENT
+END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-malformed-property.ics b/src/parser/calendar/ics/__tests__/data/err-malformed-property.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/err-malformed-property.ics
@@ -0,0 +1,5 @@
+BEGIN:VCALENDAR
+BEGIN:VEVENT
+PEANUTBUTTER&JELLY:sandwich
+END:VEVENT
+END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-missing-end.ics b/src/parser/calendar/ics/__tests__/data/err-missing-end.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/err-missing-end.ics
@@ -0,0 +1,2 @@
+BEGIN:VCALENDAR
+BEGIN:VEVENT
diff --git a/src/parser/calendar/ics/__tests__/data/err-missing-value.ics b/src/parser/calendar/ics/__tests__/data/err-missing-value.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/err-missing-value.ics
@@ -0,0 +1,5 @@
+BEGIN:VCALENDAR
+BEGIN:VEVENT
+TRIANGLE;color=red
+END:VEVENT
+END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-mixmatched-sections.ics b/src/parser/calendar/ics/__tests__/data/err-mixmatched-sections.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/err-mixmatched-sections.ics
@@ -0,0 +1,4 @@
+BEGIN:A
+BEGIN:B
+END:A
+END:B
diff --git a/src/parser/calendar/ics/__tests__/data/err-root-property.ics b/src/parser/calendar/ics/__tests__/data/err-root-property.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/err-root-property.ics
@@ -0,0 +1 @@
+NAME:value
diff --git a/src/parser/calendar/ics/__tests__/data/err-unescaped-backslash.ics b/src/parser/calendar/ics/__tests__/data/err-unescaped-backslash.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/err-unescaped-backslash.ics
@@ -0,0 +1,5 @@
+BEGIN:VCALENDAR
+BEGIN:VEVENT
+STORY:The duck coughed up an unescaped backslash: \
+END:VEVENT
+END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-unexpected-child.ics b/src/parser/calendar/ics/__tests__/data/err-unexpected-child.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/err-unexpected-child.ics
@@ -0,0 +1,6 @@
+BEGIN:VCALENDAR
+BEGIN:VEVENT
+BEGIN:TEST
+END:TEST
+END:VEVENT
+END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-unexpected-text.ics b/src/parser/calendar/ics/__tests__/data/err-unexpected-text.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/err-unexpected-text.ics
@@ -0,0 +1,5 @@
+BEGIN:VCALENDAR
+BEGIN:VEVENT
+SQUARE;color=red"
+END:VEVENT
+END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/good-boolean.ics b/src/parser/calendar/ics/__tests__/data/good-boolean.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/good-boolean.ics
@@ -0,0 +1,5 @@
+BEGIN:VCALENDAR
+BEGIN:VEVENT
+DUCK;VALUE=BOOLEAN:TRUE
+END:VEVENT
+END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/multiple-vcalendars.ics b/src/parser/calendar/ics/__tests__/data/multiple-vcalendars.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/multiple-vcalendars.ics
@@ -0,0 +1,4 @@
+BEGIN:VCALENDAR
+END:VCALENDAR
+BEGIN:VCALENDAR
+END:VCALENDAR

File Metadata

Mime Type
text/plain
Expires
May 16 2024, 12:13 AM (4 w, 2 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6285583
Default Alt Text
D16521.diff (22 KB)

Event Timeline