) will be sorted instead
+ * of the itself.
+ */
+jQuery.fn.sortElements = (function(){
+
+ var sort = [].sort;
+
+ return function(comparator, getSortable) {
+
+ getSortable = getSortable || function(){return this;};
+
+ var placements = this.map(function(){
+
+ var sortElement = getSortable.call(this),
+ parentNode = sortElement.parentNode,
+
+ // Since the element itself will change position, we have
+ // to have some way of storing it's original position in
+ // the DOM. The easiest way is to have a 'flag' node:
+ nextSibling = parentNode.insertBefore(
+ document.createTextNode(''),
+ sortElement.nextSibling
+ );
+
+ return function() {
+
+ if (parentNode === this) {
+ throw new Error(
+ "You can't sort elements if any one is a descendant of another."
+ );
+ }
+
+ // Insert before flag:
+ parentNode.insertBefore(this, nextSibling);
+ // Remove flag:
+ parentNode.removeChild(nextSibling);
+
+ };
+
+ });
+
+ return sort.call(this, comparator).each(function(i){
+ placements[i].call(getSortable.call(this));
+ });
+
+ };
+
+})();
+ $(window).load(function() {
+ var $document = $(document);
+ var $left = $('#left');
+ var $right = $('#right');
+ var $rightInner = $('#rightInner');
+ var $splitter = $('#splitter');
+ var $groups = $('#groups');
+ var $content = $('#content');
+
+ // Menu
+
+ // Hide deep packages and namespaces
+ $('ul span', $groups).click(function(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ $(this)
+ .toggleClass('collapsed')
+ .parent()
+ .next('ul')
+ .toggleClass('collapsed');
+ }).click();
+
+ $active = $('ul li.active', $groups);
+ if ($active.length > 0) {
+ // Open active
+ $('> a > span', $active).click();
+ } else {
+ $main = $('> ul > li.main', $groups);
+ if ($main.length > 0) {
+ // Open first level of the main project
+ $('> a > span', $main).click();
+ } else {
+ // Open first level of all
+ $('> ul > li > a > span', $groups).click();
+ }
+ }
+
+ // Content
+
+ // Search autocompletion
+ var autocompleteFound = false;
+ var autocompleteFiles = {'c': 'class', 'co': 'constant', 'f': 'function', 'm': 'class', 'mm': 'class', 'p': 'class', 'mp': 'class', 'cc': 'class'};
+ var $search = $('#search input[name=q]');
+ $search
+ .autocomplete(ApiGen.elements, {
+ matchContains: true,
+ scrollHeight: 200,
+ max: 20,
+ noRecord: '',
+ highlight: function(value, term) {
+ var term = term.toUpperCase().replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1").replace(/[A-Z0-9]/g, function(m, offset) {
+ return offset === 0 ? '(?:' + m + '|^' + m.toLowerCase() + ')' : '(?:(?:[^<>]|<[^<>]*>)*' + m + '|' + m.toLowerCase() + ')';
+ });
+ return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)"), "$1 ");
+ },
+ formatItem: function(data) {
+ return data.length > 1 ? data[1].replace(/^(.+\\)(.+)$/, '$1 $2 ') : data[0];
+ },
+ formatMatch: function(data) {
+ return data[1];
+ },
+ formatResult: function(data) {
+ return data[1];
+ },
+ show: function($list) {
+ var $items = $('li span', $list);
+ var maxWidth = Math.max.apply(null, $items.map(function() {
+ return $(this).width();
+ }));
+ // 10px padding
+ $list
+ .width(Math.max(maxWidth + 10, $search.innerWidth()))
+ .css('left', $search.offset().left + $search.outerWidth() - $list.outerWidth());
+ }
+ }).result(function(event, data) {
+ autocompleteFound = true;
+ var location = window.location.href.split('/');
+ location.pop();
+ var parts = data[1].split(/::|$/);
+ var file = $.sprintf(ApiGen.config.templates[autocompleteFiles[data[0]]].filename, parts[0].replace(/\(\)/, '').replace(/[^\w]/g, '.'));
+ if (parts[1]) {
+ file += '#' + ('mm' === data[0] || 'mp' === data[0] ? 'm' : '') + parts[1].replace(/([\w]+)\(\)/, '_$1');
+ }
+ location.push(file);
+ window.location = location.join('/');
+
+ // Workaround for Opera bug
+ $(this).closest('form').attr('action', location.join('/'));
+ }).closest('form')
+ .submit(function() {
+ var query = $search.val();
+ if ('' === query) {
+ return false;
+ }
+ return !autocompleteFound && '' !== $('#search input[name=cx]').val();
+ });
+
+ // Save natural order
+ $('table.summary tr[data-order]', $content).each(function(index) {
+ do {
+ index = '0' + index;
+ } while (index.length < 3);
+ $(this).attr('data-order-natural', index);
+ });
+
+ // Switch between natural and alphabetical order
+ var $caption = $('table.summary', $content)
+ .filter(':has(tr[data-order])')
+ .find('caption');
+ $caption
+ .click(function() {
+ var $this = $(this);
+ var order = $this.data('order') || 'natural';
+ order = 'natural' === order ? 'alphabetical' : 'natural';
+ $this.data('order', order);
+ $.cookie('order', order, {expires: 365});
+ var attr = 'alphabetical' === order ? 'data-order' : 'data-order-natural';
+ $this
+ .closest('table')
+ .find('tr').sortElements(function(a, b) {
+ return $(a).attr(attr) > $(b).attr(attr) ? 1 : -1;
+ });
+ return false;
+ })
+ .addClass('switchable')
+ .attr('title', 'Switch between natural and alphabetical order');
+ if ((null === $.cookie('order') && 'alphabetical' === ApiGen.config.options.elementsOrder) || 'alphabetical' === $.cookie('order')) {
+ $caption.click();
+ }
+
+ // Open details
+ if (ApiGen.config.options.elementDetailsCollapsed) {
+ var trCollapsed = true;
+ $('tr', $content).filter(':has(.detailed)')
+ .click(function() {
+ var $this = $(this);
+ if (trCollapsed) {
+ $('.short', $this).hide();
+ $('.detailed', $this).show();
+ trCollapsed = false;
+ } else {
+ $('.short', $this).show();
+ $('.detailed', $this).hide();
+ trCollapsed = true;
+ }
+ });
+ }
+
+ // Splitter
+ var splitterWidth = $splitter.width();
+ var splitterPosition = $.cookie('splitter') ? parseInt($.cookie('splitter')) : null;
+ var splitterPositionBackup = $.cookie('splitterBackup') ? parseInt($.cookie('splitterBackup')) : null;
+ function setSplitterPosition(position)
+ {
+ splitterPosition = position;
+
+ $left.width(position);
+ $right.css('margin-left', position + splitterWidth);
+ $splitter.css('left', position);
+ }
+ function setContentWidth()
+ {
+ var width = $rightInner.width();
+ $rightInner
+ .toggleClass('medium', width <= 960)
+ .toggleClass('small', width <= 650);
+ }
+ $splitter.mousedown(function() {
+ $splitter.addClass('active');
+
+ $document.mousemove(function(event) {
+ if (event.pageX >= 230 && $document.width() - event.pageX >= 600 + splitterWidth) {
+ setSplitterPosition(event.pageX);
+ setContentWidth();
+ }
+ });
+
+ $()
+ .add($splitter)
+ .add($document)
+ .mouseup(function() {
+ $splitter
+ .removeClass('active')
+ .unbind('mouseup');
+ $document
+ .unbind('mousemove')
+ .unbind('mouseup');
+
+ $.cookie('splitter', splitterPosition, {expires: 365});
+ });
+
+ return false;
+ });
+ $splitter.dblclick(function() {
+ if (splitterPosition) {
+ splitterPositionBackup = $left.width();
+ setSplitterPosition(0);
+ } else {
+ setSplitterPosition(splitterPositionBackup);
+ splitterPositionBackup = null;
+ }
+
+ setContentWidth();
+
+ $.cookie('splitter', splitterPosition, {expires: 365});
+ $.cookie('splitterBackup', splitterPositionBackup, {expires: 365});
+ });
+ if (null !== splitterPosition) {
+ setSplitterPosition(splitterPosition);
+ }
+ setContentWidth();
+ $(window).resize(setContentWidth);
+
+ // Select selected lines
+ var matches = window.location.hash.substr(1).match(/^\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*$/);
+ if (null !== matches) {
+ var lists = matches[0].split(',');
+ for (var i = 0; i < lists.length; i++) {
+ var lines = lists[i].split('-');
+ lines[0] = parseInt(lines[0]);
+ lines[1] = parseInt(lines[1] || lines[0]);
+ for (var j = lines[0]; j <= lines[1]; j++) {
+ $('#' + j).addClass('selected');
+ }
+ }
+
+ var $firstLine = $('#' + parseInt(matches[0]));
+ if ($firstLine.length > 0) {
+ $document.scrollTop($firstLine.offset().top);
+ }
+ }
+
+ // Save selected lines
+ var lastLine;
+ $('.l a').click(function(event) {
+ event.preventDefault();
+
+ var selectedLine = $(this).parent().index() + 1;
+ var $selectedLine = $('pre.code .l').eq(selectedLine - 1);
+
+ if (event.shiftKey) {
+ if (lastLine) {
+ for (var i = Math.min(selectedLine, lastLine); i <= Math.max(selectedLine, lastLine); i++) {
+ $('#' + i).addClass('selected');
+ }
+ } else {
+ $selectedLine.addClass('selected');
+ }
+ } else if (event.ctrlKey) {
+ $selectedLine.toggleClass('selected');
+ } else {
+ var $selected = $('.l.selected')
+ .not($selectedLine)
+ .removeClass('selected');
+ if ($selected.length > 0) {
+ $selectedLine.addClass('selected');
+ } else {
+ $selectedLine.toggleClass('selected');
+ }
+ }
+
+ lastLine = $selectedLine.hasClass('selected') ? selectedLine : null;
+
+ // Update hash
+ var lines = $('.l.selected')
+ .map(function() {
+ return parseInt($(this).attr('id'));
+ })
+ .get()
+ .sort(function(a, b) {
+ return a - b;
+ });
+
+ var hash = [];
+ var list = [];
+ for (var j = 0; j < lines.length; j++) {
+ if (0 === j && j + 1 === lines.length) {
+ hash.push(lines[j]);
+ } else if (0 === j) {
+ list[0] = lines[j];
+ } else if (lines[j - 1] + 1 !== lines[j] && j + 1 === lines.length) {
+ hash.push(list.join('-'));
+ hash.push(lines[j]);
+ } else if (lines[j - 1] + 1 !== lines[j]) {
+ hash.push(list.join('-'));
+ list = [lines[j]];
+ } else if (j + 1 === lines.length) {
+ list[1] = lines[j];
+ hash.push(list.join('-'));
+ } else {
+ list[1] = lines[j];
+ }
+ }
+
+ hash = hash.join(',');
+ $backup = $('#' + hash).removeAttr('id');
+ window.location.hash = hash;
+ $backup.attr('id', hash);
+ });
+});
+
diff --git a/resources/footer.png b/resources/footer.png
new file mode 100644
index 0000000..d99890c
Binary files /dev/null and b/resources/footer.png differ
diff --git a/resources/inherit.png b/resources/inherit.png
new file mode 100644
index 0000000..957079b
Binary files /dev/null and b/resources/inherit.png differ
diff --git a/resources/resize.png b/resources/resize.png
new file mode 100644
index 0000000..fb98a7a
Binary files /dev/null and b/resources/resize.png differ
diff --git a/resources/sort.png b/resources/sort.png
new file mode 100644
index 0000000..0d0fea1
Binary files /dev/null and b/resources/sort.png differ
diff --git a/resources/style.css b/resources/style.css
new file mode 100644
index 0000000..8bf17b5
--- /dev/null
+++ b/resources/style.css
@@ -0,0 +1,619 @@
+body {
+ font: 13px/1.5 Verdana, 'Geneva CE', lucida, sans-serif;
+ margin: 0;
+ padding: 0;
+ background: #ffffff;
+ color: #333333;
+}
+
+h1, h2, h3, h4, caption {
+ font-family: 'Trebuchet MS', 'Geneva CE', lucida, sans-serif;
+ color: #053368;
+}
+
+h1 {
+ color: #1e5eb6;
+ font-size: 230%;
+ font-weight: normal;
+ margin: .3em 0;
+}
+
+h2 {
+ color: #1e5eb6;
+ font-size: 150%;
+ font-weight: normal;
+ margin: -.3em 0 .3em 0;
+}
+
+h3 {
+ font-size: 1.6em;
+ font-weight: normal;
+ margin-bottom: 2px;
+}
+
+h4 {
+ font-size: 100%;
+ font-weight: bold;
+ padding: 0;
+ margin: 0;
+}
+
+caption {
+ border: 1px solid #cccccc;
+ background: #ecede5;
+ font-weight: bold;
+ font-size: 1.2em;
+ padding: 3px 5px;
+ text-align: left;
+ margin-bottom: 0;
+}
+
+p {
+ margin: .7em 0 1em;
+ padding: 0;
+}
+
+hr {
+ margin: 2em 0 1em;
+ border: none;
+ border-top: 1px solid #cccccc;
+ height: 0;
+}
+
+a {
+ color: #006aeb;
+ padding: 3px 1px;
+ text-decoration: none;
+}
+
+h1 a {
+ color: #1e5eb6;
+}
+
+a:hover, a:active, a:focus, a:hover b, a:hover var {
+ background-color: #006aeb;
+ color: #ffffff !important;
+}
+
+code, var, pre {
+ font-family: monospace;
+}
+
+var {
+ font-weight: bold;
+ font-style: normal;
+ color: #ca8a04;
+}
+
+pre {
+ margin: 0;
+}
+
+code a b {
+ color: #000000;
+}
+
+.deprecated {
+ text-decoration: line-through;
+ opacity: .5;
+}
+
+.invalid {
+ color: #e71818;
+}
+
+.hidden {
+ display: none;
+}
+
+/* Left side */
+#left {
+ overflow: auto;
+ width: 270px;
+ height: 100%;
+ position: fixed;
+}
+
+/* Menu */
+#menu {
+ padding: 10px;
+}
+
+#menu ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+#menu ul ul {
+ padding-left: 10px;
+}
+
+#menu li {
+ white-space: nowrap;
+ position: relative;
+}
+
+#menu a {
+ display: block;
+ padding: 0 2px;
+}
+
+#menu .active > a, #menu > span {
+ color: #333333;
+ background: none;
+ font-weight: bold;
+}
+
+#menu .active > a.invalid {
+ color: #e71818;
+}
+
+#menu .active > a:hover, #menu .active > a:active, #menu .active > a:focus {
+ background-color: #006aeb;
+}
+
+#menu #groups span {
+ position: absolute;
+ top: 4px;
+ right: 2px;
+ cursor: pointer;
+ display: block;
+ width: 12px;
+ height: 12px;
+ background: url('collapsed.png') transparent 0 0 no-repeat;
+}
+
+#menu #groups span:hover {
+ background-position: -12px 0;
+}
+
+#menu #groups span.collapsed {
+ background-position: 0 -12px;
+}
+
+#menu #groups span.collapsed:hover {
+ background-position: -12px -12px;
+}
+
+#menu #groups ul.collapsed {
+ display: none;
+}
+
+/* Right side */
+#right {
+ overflow: auto;
+ margin-left: 275px;
+ height: 100%;
+ position: relative;
+ left: 0;
+ right: 0;
+}
+
+#rightInner {
+ max-width: 1000px;
+ min-width: 350px;
+}
+
+/* Search */
+#search {
+ float: right;
+ margin: 3px 8px;
+}
+
+#search input.text {
+ padding: 3px 5px;
+ width: 250px;
+}
+
+/* Autocomplete */
+.ac_results {
+ padding: 0;
+ border: 1px solid #cccccc;
+ background-color: #ffffff;
+ overflow: hidden;
+ z-index: 99999;
+}
+
+.ac_results ul {
+ width: 100%;
+ list-style-position: outside;
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.ac_results li {
+ margin: 0;
+ padding: 2px 5px;
+ cursor: default;
+ display: block;
+ font: 12px 'Trebuchet MS', 'Geneva CE', lucida, sans-serif;
+ line-height: 16px;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.ac_results li strong {
+ color: #000000;
+}
+
+.ac_odd {
+ background-color: #eeeeee;
+}
+
+.ac_over {
+ background-color: #006aeb;
+ color: #ffffff;
+}
+
+.ac_results li.ac_over strong {
+ color: #ffffff;
+}
+
+/* Navigation */
+#navigation {
+ padding: 3px 8px;
+ background-color: #f6f6f4;
+ height: 26px;
+}
+
+#navigation ul {
+ list-style: none;
+ margin: 0 8px 4px 0;
+ padding: 0;
+ overflow: hidden;
+ float: left;
+}
+
+#navigation ul + ul {
+ border-left: 1px solid #000000;
+ padding-left: 8px;
+}
+
+#navigation ul li {
+ float: left;
+ margin: 2px;
+ padding: 0 3px;
+ font-family: Verdana, 'Geneva CE', lucida, sans-serif;
+ color: #808080;
+}
+
+#navigation ul li.active {
+ background-color: #053368;
+ color: #ffffff;
+ font-weight: bold;
+}
+
+#navigation ul li a {
+ color: #000000;
+ font-weight: bold;
+ padding: 0;
+}
+
+#navigation ul li span {
+ float: left;
+ padding: 0 3px;
+}
+
+#navigation ul li a:hover span, #navigation ul li a:active span, #navigation ul li a:focus span {
+ background-color: #006aeb;
+}
+
+/* Content */
+#content {
+ clear: both;
+ padding: 5px 15px;
+}
+
+.description pre {
+ padding: .6em;
+ background: #fcfcf7;
+}
+
+#content > .description {
+ background: #ecede5;
+ padding: 1px 8px;
+ margin: 1.2em 0;
+}
+
+#content > .description pre {
+ margin: .5em 0;
+}
+
+dl.tree {
+ margin: 1.2em 0;
+}
+
+dl.tree dd {
+ margin: 0;
+ padding: 0;
+}
+
+.info {
+ margin: 1.2em 0;
+}
+
+.summary {
+ border: 1px solid #cccccc;
+ border-collapse: collapse;
+ font-size: 1em;
+ width: 100%;
+ margin: 1.2em 0 2.4em;
+}
+
+.summary caption {
+ border-width: 1px 1px 0;
+}
+
+.summary caption.switchable {
+ background: #ecede5 url('sort.png') no-repeat center right;
+ cursor: pointer;
+}
+
+.summary td {
+ border: 1px solid #cccccc;
+ margin: 0;
+ padding: 3px 10px;
+ font-size: 1em;
+ vertical-align: top;
+}
+
+.summary td:first-child {
+ text-align: right;
+}
+
+.summary td hr {
+ margin: 3px -10px;
+}
+
+#packages.summary td:first-child, #namespaces.summary td:first-child, .inherited.summary td:first-child, .used.summary td:first-child {
+ text-align: left;
+}
+
+.summary tr:hover td {
+ background: #f6f6f4;
+}
+
+.summary .description pre {
+ border: .5em solid #ecede5;
+}
+
+.summary .description p {
+ margin: 0;
+}
+
+.summary .description p + p, .summary .description ul {
+ margin: 3px 0 0 0;
+}
+
+.summary .description.detailed h4 {
+ margin-top: 3px;
+}
+
+.summary dl {
+ margin: 0;
+}
+
+.summary dd {
+ margin: 0 0 0 25px;
+}
+
+.name, .attributes {
+ white-space: nowrap;
+}
+
+.value code {
+ white-space: pre-wrap;
+}
+
+td.name, td.attributes {
+ width: 1%;
+}
+
+td.attributes {
+ width: 1%;
+}
+
+.class .methods .name, .class .properties .name, .class .constants .name {
+ width: auto;
+ white-space: normal;
+}
+
+.class .methods .name > div > code {
+ white-space: pre-wrap;
+}
+
+.class .methods .name > div > code span, .function .value > code {
+ white-space: nowrap;
+ display: inline-block;
+}
+
+.class .methods td.name > div, .class td.value > div {
+ position: relative;
+ padding-right: 1em;
+}
+
+.anchor {
+ position: absolute;
+ top: 0;
+ right: 0;
+ line-height: 1;
+ font-size: 85%;
+ margin: 0;
+ color: #006aeb !important;
+}
+
+.list {
+ margin: 0 0 5px 25px;
+}
+
+div.invalid {
+ background-color: #fae4e0;
+ padding: 10px;
+}
+
+/* Splitter */
+#splitter {
+ position: fixed;
+ height: 100%;
+ width: 5px;
+ left: 270px;
+ background: #1e5eb6 url('resize.png') left center no-repeat;
+ cursor: e-resize;
+}
+
+#splitter.active {
+ opacity: .5;
+}
+
+/* Footer */
+#footer {
+ border-top: 1px solid #e9eeef;
+ clear: both;
+ color: #a7a7a7;
+ font-size: 8pt;
+ text-align: center;
+ padding: 20px 0 0;
+ margin: 3em 0 0;
+ height: 90px;
+ background: #ffffff url('footer.png') no-repeat center top;
+}
+
+/* Tree */
+div.tree ul {
+ list-style: none;
+ background: url('tree-vertical.png') left repeat-y;
+ padding: 0;
+ margin-left: 20px;
+}
+
+div.tree li {
+ margin: 0;
+ padding: 0;
+}
+
+div.tree div {
+ padding-left: 30px;
+}
+
+div.tree div.notlast {
+ background: url('tree-hasnext.png') left 10px no-repeat;
+}
+
+div.tree div.last {
+ background: url('tree-last.png') left -240px no-repeat;
+}
+
+div.tree li.last {
+ background: url('tree-cleaner.png') left center repeat-y;
+}
+
+div.tree span.padding {
+ padding-left: 15px;
+}
+
+/* Source code */
+.php-keyword1 {
+ color: #e71818;
+ font-weight: bold;
+}
+
+.php-keyword2 {
+ font-weight: bold;
+}
+
+.php-var {
+ color: #d59401;
+ font-weight: bold;
+}
+
+.php-num {
+ color: #cd0673;
+}
+
+.php-quote {
+ color: #008000;
+}
+
+.php-comment {
+ color: #929292;
+}
+
+.xlang {
+ color: #ff0000;
+ font-weight: bold;
+}
+
+pre.numbers {
+ float: left;
+}
+
+span.l {
+ display: block;
+}
+
+span.l.selected {
+ background: #f6f6f4;
+}
+
+span.l a {
+ color: #333333;
+}
+
+span.l a:hover, div.l a:active, div.l a:focus {
+ background: transparent;
+ color: #333333 !important;
+}
+
+span.l .php-var a {
+ color: #d59401;
+}
+
+span.l .php-var a:hover, span.l .php-var a:active, span.l .php-var a:focus {
+ color: #d59401 !important;
+}
+
+span.l a.l {
+ padding-left: 2px;
+ color: #c0c0c0;
+}
+
+span.l a.l:hover, span.l a.l:active, span.l a.l:focus {
+ background: transparent;
+ color: #c0c0c0 !important;
+}
+
+#rightInner.medium #navigation {
+ height: 52px;
+}
+
+#rightInner.medium #navigation ul:first-child + ul {
+ clear: left;
+ border: none;
+ padding: 0;
+}
+
+#rightInner.medium .name, #rightInner.medium .attributes {
+ white-space: normal;
+}
+
+#rightInner.small #search {
+ float: left;
+}
+
+#rightInner.small #navigation {
+ height: 78px;
+}
+
+#rightInner.small #navigation ul:first-child {
+ clear: both;
+}
+
+/* global style */
+.left, .summary td.left {
+ text-align: left;
+}
+.right, .summary td.right {
+ text-align: right;
+}
diff --git a/resources/tree-cleaner.png b/resources/tree-cleaner.png
new file mode 100644
index 0000000..2eb9085
Binary files /dev/null and b/resources/tree-cleaner.png differ
diff --git a/resources/tree-hasnext.png b/resources/tree-hasnext.png
new file mode 100644
index 0000000..91d6b79
Binary files /dev/null and b/resources/tree-hasnext.png differ
diff --git a/resources/tree-last.png b/resources/tree-last.png
new file mode 100644
index 0000000..7f319f8
Binary files /dev/null and b/resources/tree-last.png differ
diff --git a/resources/tree-vertical.png b/resources/tree-vertical.png
new file mode 100644
index 0000000..384908b
Binary files /dev/null and b/resources/tree-vertical.png differ
diff --git a/source-class-splitbrain.PHPArchive.Archive.html b/source-class-splitbrain.PHPArchive.Archive.html
new file mode 100644
index 0000000..96de26b
--- /dev/null
+++ b/source-class-splitbrain.PHPArchive.Archive.html
@@ -0,0 +1,236 @@
+
+
+
+
+
+
+ File Archive.php
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/source-class-splitbrain.PHPArchive.ArchiveCorruptedException.html b/source-class-splitbrain.PHPArchive.ArchiveCorruptedException.html
new file mode 100644
index 0000000..192e83e
--- /dev/null
+++ b/source-class-splitbrain.PHPArchive.ArchiveCorruptedException.html
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+ File ArchiveCorruptedException.php
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
1: 2: 3: 4: 5: 6: 7: 8: 9: 10:
+
<?php
+
+ namespace splitbrain\PHPArchive;
+
+
+ class ArchiveCorruptedException extends \Exception
+{
+ }
+
+
+
+
+
+
+
+
diff --git a/source-class-splitbrain.PHPArchive.ArchiveIOException.html b/source-class-splitbrain.PHPArchive.ArchiveIOException.html
new file mode 100644
index 0000000..619c85f
--- /dev/null
+++ b/source-class-splitbrain.PHPArchive.ArchiveIOException.html
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+ File ArchiveIOException.php
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
1: 2: 3: 4: 5: 6: 7: 8: 9: 10:
+
<?php
+
+ namespace splitbrain\PHPArchive;
+
+
+ class ArchiveIOException extends \Exception
+{
+ }
+
+
+
+
+
+
+
+
diff --git a/source-class-splitbrain.PHPArchive.ArchiveIllegalCompressionException.html b/source-class-splitbrain.PHPArchive.ArchiveIllegalCompressionException.html
new file mode 100644
index 0000000..5d48c96
--- /dev/null
+++ b/source-class-splitbrain.PHPArchive.ArchiveIllegalCompressionException.html
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+ File ArchiveIllegalCompressionException.php
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
1: 2: 3: 4: 5: 6: 7: 8: 9: 10:
+
<?php
+
+ namespace splitbrain\PHPArchive;
+
+
+ class ArchiveIllegalCompressionException extends \Exception
+{
+ }
+
+
+
+
+
+
+
+
diff --git a/source-class-splitbrain.PHPArchive.FileInfo.html b/source-class-splitbrain.PHPArchive.FileInfo.html
new file mode 100644
index 0000000..aceb094
--- /dev/null
+++ b/source-class-splitbrain.PHPArchive.FileInfo.html
@@ -0,0 +1,459 @@
+
+
+
+
+
+
+ File FileInfo.php
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 303: 304: 305: 306: 307: 308: 309: 310: 311: 312: 313: 314: 315: 316: 317: 318: 319: 320: 321: 322: 323: 324: 325: 326: 327: 328: 329: 330: 331: 332: 333: 334: 335: 336: 337: 338: 339: 340: 341: 342: 343: 344: 345: 346: 347: 348: 349: 350: 351: 352: 353: 354: 355: 356: 357: 358: 359:
+
<?php
+
+ namespace splitbrain\PHPArchive;
+
+
+ class FileInfo
+{
+
+ protected $isdir = false ;
+ protected $path = '' ;
+ protected $size = 0 ;
+ protected $csize = 0 ;
+ protected $mtime = 0 ;
+ protected $mode = 0664 ;
+ protected $owner = '' ;
+ protected $group = '' ;
+ protected $uid = 0 ;
+ protected $gid = 0 ;
+ protected $comment = '' ;
+
+
+ public function __construct($path = '' )
+ {
+ $this ->mtime = time ();
+ $this ->setPath($path );
+ }
+
+
+ public function __call($name , $arguments )
+ {
+ if ($name === 'match' ) {
+ trigger_error ('FileInfo::match() is deprecated, use FileInfo::matchExpression() instead.' , E_USER_NOTICE);
+ return call_user_func_array ([$this , $name ], $arguments );
+ }
+
+ trigger_error ('Call to undefined method FileInfo::' .$name .'()' , E_USER_ERROR);
+ return null ;
+ }
+
+
+ public static function fromPath($path , $as = '' )
+ {
+ clearstatcache (false , $path );
+
+ if (!file_exists ($path )) {
+ throw new FileInfoException(" $path does not exist" );
+ }
+
+ $stat = stat ($path );
+ $file = new FileInfo();
+
+ $file ->setPath($path );
+ $file ->setIsdir(is_dir ($path ));
+ $file ->setMode(fileperms ($path ));
+ $file ->setOwner(fileowner ($path ));
+ $file ->setGroup(filegroup ($path ));
+ $file ->setSize(filesize ($path ));
+ $file ->setUid($stat ['uid' ]);
+ $file ->setGid($stat ['gid' ]);
+ $file ->setMtime($stat ['mtime' ]);
+
+ if ($as ) {
+ $file ->setPath($as );
+ }
+
+ return $file ;
+ }
+
+
+ public function getSize()
+ {
+ if ($this ->isdir) return 0 ;
+ return $this ->size;
+ }
+
+
+ public function setSize($size )
+ {
+ $this ->size = $size ;
+ }
+
+
+ public function getCompressedSize()
+ {
+ return $this ->csize;
+ }
+
+
+ public function setCompressedSize($csize )
+ {
+ $this ->csize = $csize ;
+ }
+
+
+ public function getMtime()
+ {
+ return $this ->mtime;
+ }
+
+
+ public function setMtime($mtime )
+ {
+ $this ->mtime = $mtime ;
+ }
+
+
+ public function getGid()
+ {
+ return $this ->gid;
+ }
+
+
+ public function setGid($gid )
+ {
+ $this ->gid = $gid ;
+ }
+
+
+ public function getUid()
+ {
+ return $this ->uid;
+ }
+
+
+ public function setUid($uid )
+ {
+ $this ->uid = $uid ;
+ }
+
+
+ public function getComment()
+ {
+ return $this ->comment;
+ }
+
+
+ public function setComment($comment )
+ {
+ $this ->comment = $comment ;
+ }
+
+
+ public function getGroup()
+ {
+ return $this ->group;
+ }
+
+
+ public function setGroup($group )
+ {
+ $this ->group = $group ;
+ }
+
+
+ public function getIsdir()
+ {
+ return $this ->isdir;
+ }
+
+
+ public function setIsdir($isdir )
+ {
+
+ if ($isdir && $this ->mode === 0664 ) {
+ $this ->mode = 0775 ;
+ }
+ $this ->isdir = $isdir ;
+ }
+
+
+ public function getMode()
+ {
+ return $this ->mode;
+ }
+
+
+ public function setMode($mode )
+ {
+ $this ->mode = $mode ;
+ }
+
+
+ public function getOwner()
+ {
+ return $this ->owner;
+ }
+
+
+ public function setOwner($owner )
+ {
+ $this ->owner = $owner ;
+ }
+
+
+ public function getPath()
+ {
+ return $this ->path;
+ }
+
+
+ public function setPath($path )
+ {
+ $this ->path = $this ->cleanPath($path );
+ }
+
+
+ protected function cleanPath($path )
+ {
+ $path = str_replace ('\\' , '/' , $path );
+ $path = explode ('/' , $path );
+ $newpath = array ();
+ foreach ($path as $p ) {
+ if ($p === '' || $p === '.' ) {
+ continue ;
+ }
+ if ($p === '..' ) {
+ array_pop ($newpath );
+ continue ;
+ }
+ array_push ($newpath , $p );
+ }
+ return trim (implode ('/' , $newpath ), '/' );
+ }
+
+
+ public function strip($strip )
+ {
+ $filename = $this ->getPath();
+ $striplen = strlen ($strip );
+ if (is_int ($strip )) {
+
+ $parts = explode ('/' , $filename );
+ if (!$this ->getIsdir()) {
+ $base = array_pop ($parts );
+ } else {
+ $base = '' ;
+ }
+ $filename = join ('/' , array_slice ($parts , $strip ));
+ if ($base ) {
+ $filename .= "/ $base " ;
+ }
+ } else {
+
+ if (substr ($filename , 0 , $striplen ) == $strip ) {
+ $filename = substr ($filename , $striplen );
+ }
+ }
+
+ $this ->setPath($filename );
+ }
+
+
+ public function matchExpression($include = '' , $exclude = '' )
+ {
+ $extract = true ;
+ if ($include && !preg_match ($include , $this ->getPath())) {
+ $extract = false ;
+ }
+ if ($exclude && preg_match ($exclude , $this ->getPath())) {
+ $extract = false ;
+ }
+
+ return $extract ;
+ }
+ }
+
+
+
+
+
+
+
+
+
+
diff --git a/source-class-splitbrain.PHPArchive.FileInfoException.html b/source-class-splitbrain.PHPArchive.FileInfoException.html
new file mode 100644
index 0000000..069068f
--- /dev/null
+++ b/source-class-splitbrain.PHPArchive.FileInfoException.html
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+ File FileInfoException.php
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
1: 2: 3: 4: 5: 6: 7: 8: 9: 10:
+
<?php
+
+ namespace splitbrain\PHPArchive;
+
+
+ class FileInfoException extends \Exception
+{
+ }
+
+
+
+
+
+
+
+
diff --git a/source-class-splitbrain.PHPArchive.Tar.html b/source-class-splitbrain.PHPArchive.Tar.html
new file mode 100644
index 0000000..e374b8a
--- /dev/null
+++ b/source-class-splitbrain.PHPArchive.Tar.html
@@ -0,0 +1,924 @@
+
+
+
+
+
+
+ File Tar.php
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 303: 304: 305: 306: 307: 308: 309: 310: 311: 312: 313: 314: 315: 316: 317: 318: 319: 320: 321: 322: 323: 324: 325: 326: 327: 328: 329: 330: 331: 332: 333: 334: 335: 336: 337: 338: 339: 340: 341: 342: 343: 344: 345: 346: 347: 348: 349: 350: 351: 352: 353: 354: 355: 356: 357: 358: 359: 360: 361: 362: 363: 364: 365: 366: 367: 368: 369: 370: 371: 372: 373: 374: 375: 376: 377: 378: 379: 380: 381: 382: 383: 384: 385: 386: 387: 388: 389: 390: 391: 392: 393: 394: 395: 396: 397: 398: 399: 400: 401: 402: 403: 404: 405: 406: 407: 408: 409: 410: 411: 412: 413: 414: 415: 416: 417: 418: 419: 420: 421: 422: 423: 424: 425: 426: 427: 428: 429: 430: 431: 432: 433: 434: 435: 436: 437: 438: 439: 440: 441: 442: 443: 444: 445: 446: 447: 448: 449: 450: 451: 452: 453: 454: 455: 456: 457: 458: 459: 460: 461: 462: 463: 464: 465: 466: 467: 468: 469: 470: 471: 472: 473: 474: 475: 476: 477: 478: 479: 480: 481: 482: 483: 484: 485: 486: 487: 488: 489: 490: 491: 492: 493: 494: 495: 496: 497: 498: 499: 500: 501: 502: 503: 504: 505: 506: 507: 508: 509: 510: 511: 512: 513: 514: 515: 516: 517: 518: 519: 520: 521: 522: 523: 524: 525: 526: 527: 528: 529: 530: 531: 532: 533: 534: 535: 536: 537: 538: 539: 540: 541: 542: 543: 544: 545: 546: 547: 548: 549: 550: 551: 552: 553: 554: 555: 556: 557: 558: 559: 560: 561: 562: 563: 564: 565: 566: 567: 568: 569: 570: 571: 572: 573: 574: 575: 576: 577: 578: 579: 580: 581: 582: 583: 584: 585: 586: 587: 588: 589: 590: 591: 592: 593: 594: 595: 596: 597: 598: 599: 600: 601: 602: 603: 604: 605: 606: 607: 608: 609: 610: 611: 612: 613: 614: 615: 616: 617: 618: 619: 620: 621: 622: 623: 624: 625: 626: 627: 628: 629: 630: 631: 632: 633: 634: 635: 636: 637: 638: 639: 640: 641: 642: 643: 644: 645: 646: 647: 648: 649: 650: 651: 652: 653: 654: 655: 656: 657: 658: 659: 660: 661: 662: 663: 664: 665: 666: 667: 668: 669: 670: 671: 672: 673: 674: 675: 676: 677: 678: 679: 680: 681: 682: 683: 684: 685: 686: 687: 688: 689: 690: 691: 692: 693: 694: 695: 696: 697: 698: 699: 700: 701: 702: 703: 704: 705: 706: 707: 708: 709: 710: 711: 712: 713: 714: 715: 716: 717: 718: 719: 720: 721: 722: 723: 724: 725: 726: 727: 728: 729: 730: 731: 732: 733: 734: 735: 736: 737: 738: 739: 740: 741: 742: 743: 744: 745: 746: 747: 748: 749: 750: 751: 752: 753: 754: 755: 756: 757: 758: 759: 760: 761: 762: 763: 764: 765: 766: 767: 768: 769: 770: 771: 772: 773: 774: 775: 776: 777: 778: 779: 780: 781: 782: 783: 784: 785: 786: 787: 788: 789: 790: 791: 792: 793: 794: 795: 796: 797: 798: 799: 800: 801: 802: 803: 804: 805: 806: 807: 808: 809: 810: 811: 812: 813: 814: 815: 816: 817: 818: 819: 820: 821: 822: 823: 824:
+
<?php
+
+ namespace splitbrain\PHPArchive;
+
+
+ class Tar extends Archive
+{
+ const READ_CHUNK_SIZE = 1048576 ;
+
+ protected $file = '' ;
+ protected $comptype = Archive::COMPRESS_AUTO;
+ protected $complevel = 9 ;
+ protected $fh ;
+ protected $memory = '' ;
+ protected $closed = true ;
+ protected $writeaccess = false ;
+ protected $position = 0 ;
+ protected $contentUntil = 0 ;
+ protected $skipUntil = 0 ;
+
+
+ public function setCompression($level = 9 , $type = Archive::COMPRESS_AUTO)
+ {
+ $this ->compressioncheck($type );
+ if ($level < -1 || $level > 9 ) {
+ throw new ArchiveIllegalCompressionException('Compression level should be between -1 and 9' );
+ }
+ $this ->comptype = $type ;
+ $this ->complevel = $level ;
+ if ($level == 0 ) $this ->comptype = Archive::COMPRESS_NONE;
+ if ($type == Archive::COMPRESS_NONE) $this ->complevel = 0 ;
+ }
+
+
+ public function open($file )
+ {
+ $this ->file = $file ;
+
+
+ if ($this ->comptype == Tar::COMPRESS_AUTO) {
+ $this ->setCompression($this ->complevel, $this ->filetype ($file ));
+ }
+
+
+ if ($this ->comptype === Archive::COMPRESS_GZIP) {
+ $this ->fh = @gzopen ($this ->file , 'rb' );
+ } elseif ($this ->comptype === Archive::COMPRESS_BZIP) {
+ $this ->fh = @bzopen ($this ->file , 'r' );
+ } else {
+ $this ->fh = @fopen ($this ->file , 'rb' );
+ }
+
+ if (!$this ->fh) {
+ throw new ArchiveIOException('Could not open file for reading: ' .$this ->file );
+ }
+ $this ->closed = false ;
+ $this ->position = 0 ;
+ }
+
+
+ public function contents()
+ {
+ $result = array ();
+
+ foreach ($this ->yieldContents() as $fileinfo ) {
+ $result [] = $fileinfo ;
+ }
+
+ return $result ;
+ }
+
+
+ public function yieldContents()
+ {
+ if ($this ->closed || !$this ->file ) {
+ throw new ArchiveIOException('Can not read from a closed archive' );
+ }
+
+ while ($read = $this ->readbytes(512 )) {
+ $header = $this ->parseHeader($read );
+ if (!is_array ($header )) {
+ continue ;
+ }
+
+ $this ->contentUntil = $this ->position + $header ['size' ];
+ $this ->skipUntil = $this ->position + ceil ($header ['size' ] / 512 ) * 512 ;
+
+ yield $this ->header2fileinfo($header );
+
+ $skip = $this ->skipUntil - $this ->position;
+ if ($skip > 0 ) {
+ $this ->skipbytes($skip );
+ }
+ }
+
+ $this ->close();
+ }
+
+
+ public function readCurrentEntry($length = PHP_INT_MAX)
+ {
+ $length = (int) min ($length , $this ->contentUntil - $this ->position);
+ if ($length === 0 ) {
+ return '' ;
+ }
+ return $this ->readbytes($length );
+ }
+
+
+ public function extract ($outdir , $strip = '' , $exclude = '' , $include = '' )
+ {
+ if ($this ->closed || !$this ->file ) {
+ throw new ArchiveIOException('Can not read from a closed archive' );
+ }
+
+ $outdir = rtrim ($outdir , '/' );
+ @mkdir ($outdir , 0777 , true );
+ if (!is_dir ($outdir )) {
+ throw new ArchiveIOException("Could not create directory ' $outdir '" );
+ }
+
+ $extracted = array ();
+ while ($dat = $this ->readbytes(512 )) {
+
+ $header = $this ->parseHeader($dat );
+ if (!is_array ($header )) {
+ continue ;
+ }
+ $fileinfo = $this ->header2fileinfo($header );
+
+
+ $fileinfo ->strip($strip );
+
+
+ if (!strlen ($fileinfo ->getPath()) || !$fileinfo ->matchExpression($include , $exclude )) {
+ $this ->skipbytes(ceil ($header ['size' ] / 512 ) * 512 );
+ continue ;
+ }
+
+
+ $output = $outdir .'/' .$fileinfo ->getPath();
+ $directory = ($fileinfo ->getIsdir()) ? $output : dirname ($output );
+ if (!file_exists ($directory )) {
+ mkdir ($directory , 0777 , true );
+ }
+
+
+ if (!$fileinfo ->getIsdir()) {
+ $fp = @fopen ($output , "wb" );
+ if (!$fp ) {
+ throw new ArchiveIOException('Could not open file for writing: ' .$output );
+ }
+
+ $size = floor ($header ['size' ] / 512 );
+ for ($i = 0 ; $i < $size ; $i ++) {
+ fwrite ($fp , $this ->readbytes(512 ), 512 );
+ }
+ if (($header ['size' ] % 512 ) != 0 ) {
+ fwrite ($fp , $this ->readbytes(512 ), $header ['size' ] % 512 );
+ }
+
+ fclose ($fp );
+ @touch ($output , $fileinfo ->getMtime());
+ @chmod ($output , $fileinfo ->getMode());
+ } else {
+ $this ->skipbytes(ceil ($header ['size' ] / 512 ) * 512 );
+ }
+
+ if (is_callable ($this ->callback)) {
+ call_user_func ($this ->callback, $fileinfo );
+ }
+ $extracted [] = $fileinfo ;
+ }
+
+ $this ->close();
+ return $extracted ;
+ }
+
+
+ public function create($file = '' )
+ {
+ $this ->file = $file ;
+ $this ->memory = '' ;
+ $this ->fh = 0 ;
+
+ if ($this ->file ) {
+
+ if ($this ->comptype == Archive::COMPRESS_AUTO) {
+ $this ->setCompression($this ->complevel, $this ->filetype ($file ));
+ }
+
+ if ($this ->comptype === Archive::COMPRESS_GZIP) {
+ $this ->fh = @gzopen ($this ->file , 'wb' .$this ->complevel);
+ } elseif ($this ->comptype === Archive::COMPRESS_BZIP) {
+ $this ->fh = @bzopen ($this ->file , 'w' );
+ } else {
+ $this ->fh = @fopen ($this ->file , 'wb' );
+ }
+
+ if (!$this ->fh) {
+ throw new ArchiveIOException('Could not open file for writing: ' .$this ->file );
+ }
+ }
+ $this ->writeaccess = true ;
+ $this ->closed = false ;
+ }
+
+
+ public function addFile($file , $fileinfo = '' )
+ {
+ if (is_string ($fileinfo )) {
+ $fileinfo = FileInfo::fromPath($file , $fileinfo );
+ }
+
+ if ($this ->closed) {
+ throw new ArchiveIOException('Archive has been closed, files can no longer be added' );
+ }
+
+
+ $this ->writeFileHeader($fileinfo );
+
+
+
+
+ if (!$fileinfo ->getIsdir() && $fileinfo ->getSize() > 0 ) {
+ $read = 0 ;
+ $fp = @fopen ($file , 'rb' );
+ if (!$fp ) {
+ throw new ArchiveIOException('Could not open file for reading: ' . $file );
+ }
+ while (!feof ($fp )) {
+
+ $data = fread ($fp , self::READ_CHUNK_SIZE);
+ if ($data === false ) {
+ break ;
+ }
+ if ($data === '' ) {
+ break ;
+ }
+ $dataLen = strlen ($data );
+ $read += $dataLen ;
+
+ $passLen = ($dataLen >> 9 ) << 9 ;
+ if ($passLen === $dataLen ) {
+
+ $this ->writebytes($data );
+ } else {
+
+ $this ->writebytes(substr ($data , 0 , $passLen ));
+
+ $this ->writebytes(pack ("a512" , substr ($data , $passLen )));
+ }
+ }
+ fclose ($fp );
+
+ if ($read != $fileinfo ->getSize()) {
+ $this ->close();
+ throw new ArchiveCorruptedException("The size of $file changed while reading, archive corrupted. read $read expected " .$fileinfo ->getSize());
+ }
+ }
+
+ if (is_callable ($this ->callback)) {
+ call_user_func ($this ->callback, $fileinfo );
+ }
+ }
+
+
+ public function addData($fileinfo , $data )
+ {
+ if (is_string ($fileinfo )) {
+ $fileinfo = new FileInfo($fileinfo );
+ }
+
+ if ($this ->closed) {
+ throw new ArchiveIOException('Archive has been closed, files can no longer be added' );
+ }
+
+ $len = strlen ($data );
+ $fileinfo ->setSize($len );
+ $this ->writeFileHeader($fileinfo );
+
+
+ $passLen = ($len >> 9 ) << 9 ;
+ $this ->writebytes(substr ($data , 0 , $passLen ));
+ if ($passLen < $len ) {
+ $this ->writebytes(pack ("a512" , substr ($data , $passLen , 512 )));
+ }
+
+ if (is_callable ($this ->callback)) {
+ call_user_func ($this ->callback, $fileinfo );
+ }
+ }
+
+
+ public function close()
+ {
+ if ($this ->closed) {
+ return ;
+ }
+
+
+ if ($this ->writeaccess) {
+ $this ->writebytes(pack ("a512" , "" ));
+ $this ->writebytes(pack ("a512" , "" ));
+ }
+
+
+ if ($this ->file ) {
+ if ($this ->comptype === Archive::COMPRESS_GZIP) {
+ gzclose ($this ->fh);
+ } elseif ($this ->comptype === Archive::COMPRESS_BZIP) {
+ bzclose ($this ->fh);
+ } else {
+ fclose ($this ->fh);
+ }
+
+ $this ->file = '' ;
+ $this ->fh = 0 ;
+ }
+
+ $this ->writeaccess = false ;
+ $this ->closed = true ;
+ }
+
+
+ public function getArchive()
+ {
+ $this ->close();
+
+ if ($this ->comptype === Archive::COMPRESS_AUTO) {
+ $this ->comptype = Archive::COMPRESS_NONE;
+ }
+
+ if ($this ->comptype === Archive::COMPRESS_GZIP) {
+ return gzencode ($this ->memory, $this ->complevel);
+ }
+ if ($this ->comptype === Archive::COMPRESS_BZIP) {
+ return bzcompress ($this ->memory);
+ }
+ return $this ->memory;
+ }
+
+
+ public function save($file )
+ {
+ if ($this ->comptype === Archive::COMPRESS_AUTO) {
+ $this ->setCompression($this ->complevel, $this ->filetype ($file ));
+ }
+
+ if (!@file_put_contents ($file , $this ->getArchive())) {
+ throw new ArchiveIOException('Could not write to file: ' .$file );
+ }
+ }
+
+
+ protected function readbytes($length )
+ {
+ if ($this ->comptype === Archive::COMPRESS_GZIP) {
+ $ret = @gzread ($this ->fh, $length );
+ } elseif ($this ->comptype === Archive::COMPRESS_BZIP) {
+ $ret = @bzread ($this ->fh, $length );
+ } else {
+ $ret = @fread ($this ->fh, $length );
+ }
+ $this ->position += strlen ($ret );
+ return $ret ;
+ }
+
+
+ protected function writebytes($data )
+ {
+ if (!$this ->file ) {
+ $this ->memory .= $data ;
+ $written = strlen ($data );
+ } elseif ($this ->comptype === Archive::COMPRESS_GZIP) {
+ $written = @gzwrite ($this ->fh, $data );
+ } elseif ($this ->comptype === Archive::COMPRESS_BZIP) {
+ $written = @bzwrite ($this ->fh, $data );
+ } else {
+ $written = @fwrite ($this ->fh, $data );
+ }
+ if ($written === false ) {
+ throw new ArchiveIOException('Failed to write to archive stream' );
+ }
+ return $written ;
+ }
+
+
+ protected function skipbytes($bytes )
+ {
+ if ($this ->comptype === Archive::COMPRESS_GZIP) {
+ @gzseek ($this ->fh, $bytes , SEEK_CUR);
+ } elseif ($this ->comptype === Archive::COMPRESS_BZIP) {
+
+
+ while ($bytes ) {
+ $toread = min (8192 , $bytes );
+ @bzread ($this ->fh, $toread );
+ $bytes -= $toread ;
+ }
+ } else {
+ @fseek ($this ->fh, $bytes , SEEK_CUR);
+ }
+ $this ->position += $bytes ;
+ }
+
+
+ protected function writeFileHeader(FileInfo $fileinfo )
+ {
+ $this ->writeRawFileHeader(
+ $fileinfo ->getPath(),
+ $fileinfo ->getUid(),
+ $fileinfo ->getGid(),
+ $fileinfo ->getMode(),
+ $fileinfo ->getSize(),
+ $fileinfo ->getMtime(),
+ $fileinfo ->getIsdir() ? '5' : '0'
+ );
+ }
+
+
+ protected function writeRawFileHeader($name , $uid , $gid , $perm , $size , $mtime , $typeflag = '' )
+ {
+
+ $prefix = '' ;
+ $namelen = strlen ($name );
+ if ($namelen > 100 ) {
+ $file = basename ($name );
+ $dir = dirname ($name );
+ if (strlen ($file ) > 100 || strlen ($dir ) > 155 ) {
+
+ $this ->writeRawFileHeader('././@LongLink' , 0 , 0 , 0 , $namelen , 0 , 'L' );
+ for ($s = 0 ; $s < $namelen ; $s += 512 ) {
+ $this ->writebytes(pack ("a512" , substr ($name , $s , 512 )));
+ }
+ $name = substr ($name , 0 , 100 );
+ } else {
+
+ $prefix = $dir ;
+ $name = $file ;
+ }
+ }
+
+
+ $uid = sprintf ("%6s " , decoct ($uid ));
+ $gid = sprintf ("%6s " , decoct ($gid ));
+ $perm = sprintf ("%6s " , decoct ($perm ));
+ $size = self::numberEncode($size , 12 );
+ $mtime = self::numberEncode($size , 12 );
+
+ $data_first = pack ("a100a8a8a8a12A12" , $name , $perm , $uid , $gid , $size , $mtime );
+ $data_last = pack ("a1a100a6a2a32a32a8a8a155a12" , $typeflag , '' , 'ustar' , '' , '' , '' , '' , '' , $prefix , "" );
+
+ for ($i = 0 , $chks = 0 ; $i < 148 ; $i ++) {
+ $chks += ord ($data_first [$i ]);
+ }
+
+ for ($i = 156 , $chks += 256 , $j = 0 ; $i < 512 ; $i ++, $j ++) {
+ $chks += ord ($data_last [$j ]);
+ }
+
+ $this ->writebytes($data_first );
+
+ $chks = pack ("a8" , sprintf ("%6s " , decoct ($chks )));
+ $this ->writebytes($chks .$data_last );
+ }
+
+
+ protected function parseHeader($block )
+ {
+ if (!$block || strlen ($block ) != 512 ) {
+ throw new ArchiveCorruptedException('Unexpected length of header' );
+ }
+
+
+ if (trim ($block ) === '' ) return false ;
+
+ for ($i = 0 , $chks = 0 ; $i < 148 ; $i ++) {
+ $chks += ord ($block [$i ]);
+ }
+
+ for ($i = 156 , $chks += 256 ; $i < 512 ; $i ++) {
+ $chks += ord ($block [$i ]);
+ }
+
+ $header = @unpack (
+ "a100filename/a8perm/a8uid/a8gid/a12size/a12mtime/a8checksum/a1typeflag/a100link/a6magic/a2version/a32uname/a32gname/a8devmajor/a8devminor/a155prefix" ,
+ $block
+ );
+ if (!$header ) {
+ throw new ArchiveCorruptedException('Failed to parse header' );
+ }
+
+ $return ['checksum' ] = OctDec (trim ($header ['checksum' ]));
+ if ($return ['checksum' ] != $chks ) {
+ throw new ArchiveCorruptedException('Header does not match its checksum' );
+ }
+
+ $return ['filename' ] = trim ($header ['filename' ]);
+ $return ['perm' ] = OctDec (trim ($header ['perm' ]));
+ $return ['uid' ] = OctDec (trim ($header ['uid' ]));
+ $return ['gid' ] = OctDec (trim ($header ['gid' ]));
+ $return ['size' ] = self::numberDecode($header ['size' ]);
+ $return ['mtime' ] = self::numberDecode($header ['mtime' ]);
+ $return ['typeflag' ] = $header ['typeflag' ];
+ $return ['link' ] = trim ($header ['link' ]);
+ $return ['uname' ] = trim ($header ['uname' ]);
+ $return ['gname' ] = trim ($header ['gname' ]);
+
+
+ if (trim ($header ['prefix' ])) {
+ $return ['filename' ] = trim ($header ['prefix' ]).'/' .$return ['filename' ];
+ }
+
+
+ if ($return ['typeflag' ] == 'L' ) {
+
+ $filename = trim ($this ->readbytes(ceil ($return ['size' ] / 512 ) * 512 ));
+
+ $block = $this ->readbytes(512 );
+ $return = $this ->parseHeader($block );
+
+ $return ['filename' ] = $filename ;
+ }
+
+ return $return ;
+ }
+
+
+ protected function header2fileinfo($header )
+ {
+ $fileinfo = new FileInfo();
+ $fileinfo ->setPath($header ['filename' ]);
+ $fileinfo ->setMode($header ['perm' ]);
+ $fileinfo ->setUid($header ['uid' ]);
+ $fileinfo ->setGid($header ['gid' ]);
+ $fileinfo ->setSize($header ['size' ]);
+ $fileinfo ->setMtime($header ['mtime' ]);
+ $fileinfo ->setOwner($header ['uname' ]);
+ $fileinfo ->setGroup($header ['gname' ]);
+ $fileinfo ->setIsdir((bool) $header ['typeflag' ]);
+
+ return $fileinfo ;
+ }
+
+
+ protected function compressioncheck($comptype )
+ {
+ if ($comptype === Archive::COMPRESS_GZIP && !function_exists ('gzopen' )) {
+ throw new ArchiveIllegalCompressionException('No gzip support available' );
+ }
+
+ if ($comptype === Archive::COMPRESS_BZIP && !function_exists ('bzopen' )) {
+ throw new ArchiveIllegalCompressionException('No bzip2 support available' );
+ }
+ }
+
+
+ public function filetype ($file )
+ {
+
+ if (file_exists ($file ) && is_readable ($file ) && filesize ($file ) > 5 ) {
+ $fh = @fopen ($file , 'rb' );
+ if (!$fh ) return false ;
+ $magic = fread ($fh , 5 );
+ fclose ($fh );
+
+ if (strpos ($magic , "\x42\x5a" ) === 0 ) return Archive::COMPRESS_BZIP;
+ if (strpos ($magic , "\x1f\x8b" ) === 0 ) return Archive::COMPRESS_GZIP;
+ }
+
+
+ $file = strtolower ($file );
+ if (substr ($file , -3 ) == '.gz' || substr ($file , -4 ) == '.tgz' ) {
+ return Archive::COMPRESS_GZIP;
+ } elseif (substr ($file , -4 ) == '.bz2' || substr ($file , -4 ) == '.tbz' ) {
+ return Archive::COMPRESS_BZIP;
+ }
+
+ return Archive::COMPRESS_NONE;
+ }
+
+
+ static public function numberDecode($field )
+ {
+ $firstByte = ord (substr ($field , 0 , 1 ));
+ if ($firstByte === 255 ) {
+ $value = -1 << (8 * strlen ($field ));
+ $shift = 0 ;
+ for ($i = strlen ($field ) - 1 ; $i >= 0 ; $i --) {
+ $value += ord (substr ($field , $i , 1 )) << $shift ;
+ $shift += 8 ;
+ }
+ } elseif ($firstByte === 128 ) {
+ $value = 0 ;
+ $shift = 0 ;
+ for ($i = strlen ($field ) - 1 ; $i > 0 ; $i --) {
+ $value += ord (substr ($field , $i , 1 )) << $shift ;
+ $shift += 8 ;
+ }
+ } else {
+ $value = octdec (trim ($field ));
+ }
+ return $value ;
+ }
+
+
+ static public function numberEncode($value , $length )
+ {
+
+
+ $maxValue = 1 << (($length - 1 ) * 3 );
+ if ($value < 0 ) {
+
+ $value = pack (PHP_INT_SIZE === 8 ? 'J' : 'N' , (int) $value );
+ $encoded = str_repeat (chr (255 ), max (1 , $length - PHP_INT_SIZE));
+ $encoded .= substr ($value , max (0 , PHP_INT_SIZE - $length + 1 ));
+ } elseif ($value >= $maxValue ) {
+ $value = pack (PHP_INT_SIZE === 8 ? 'J' : 'N' , (int) $value );
+ $encoded = chr (128 ) . str_repeat (chr (0 ), max (0 , $length - PHP_INT_SIZE - 1 ));
+ $encoded .= substr ($value , max (0 , PHP_INT_SIZE - $length + 1 ));
+ } else {
+ $encoded = sprintf ("%" . ($length - 1 ) . "s " , decoct ($value ));
+ }
+ return $encoded ;
+ }
+ }
+
+
+
+
+
+
+
+
+
+
diff --git a/source-class-splitbrain.PHPArchive.Zip.html b/source-class-splitbrain.PHPArchive.Zip.html
new file mode 100644
index 0000000..6a24c11
--- /dev/null
+++ b/source-class-splitbrain.PHPArchive.Zip.html
@@ -0,0 +1,1127 @@
+
+
+
+
+
+
+ File Zip.php
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 303: 304: 305: 306: 307: 308: 309: 310: 311: 312: 313: 314: 315: 316: 317: 318: 319: 320: 321: 322: 323: 324: 325: 326: 327: 328: 329: 330: 331: 332: 333: 334: 335: 336: 337: 338: 339: 340: 341: 342: 343: 344: 345: 346: 347: 348: 349: 350: 351: 352: 353: 354: 355: 356: 357: 358: 359: 360: 361: 362: 363: 364: 365: 366: 367: 368: 369: 370: 371: 372: 373: 374: 375: 376: 377: 378: 379: 380: 381: 382: 383: 384: 385: 386: 387: 388: 389: 390: 391: 392: 393: 394: 395: 396: 397: 398: 399: 400: 401: 402: 403: 404: 405: 406: 407: 408: 409: 410: 411: 412: 413: 414: 415: 416: 417: 418: 419: 420: 421: 422: 423: 424: 425: 426: 427: 428: 429: 430: 431: 432: 433: 434: 435: 436: 437: 438: 439: 440: 441: 442: 443: 444: 445: 446: 447: 448: 449: 450: 451: 452: 453: 454: 455: 456: 457: 458: 459: 460: 461: 462: 463: 464: 465: 466: 467: 468: 469: 470: 471: 472: 473: 474: 475: 476: 477: 478: 479: 480: 481: 482: 483: 484: 485: 486: 487: 488: 489: 490: 491: 492: 493: 494: 495: 496: 497: 498: 499: 500: 501: 502: 503: 504: 505: 506: 507: 508: 509: 510: 511: 512: 513: 514: 515: 516: 517: 518: 519: 520: 521: 522: 523: 524: 525: 526: 527: 528: 529: 530: 531: 532: 533: 534: 535: 536: 537: 538: 539: 540: 541: 542: 543: 544: 545: 546: 547: 548: 549: 550: 551: 552: 553: 554: 555: 556: 557: 558: 559: 560: 561: 562: 563: 564: 565: 566: 567: 568: 569: 570: 571: 572: 573: 574: 575: 576: 577: 578: 579: 580: 581: 582: 583: 584: 585: 586: 587: 588: 589: 590: 591: 592: 593: 594: 595: 596: 597: 598: 599: 600: 601: 602: 603: 604: 605: 606: 607: 608: 609: 610: 611: 612: 613: 614: 615: 616: 617: 618: 619: 620: 621: 622: 623: 624: 625: 626: 627: 628: 629: 630: 631: 632: 633: 634: 635: 636: 637: 638: 639: 640: 641: 642: 643: 644: 645: 646: 647: 648: 649: 650: 651: 652: 653: 654: 655: 656: 657: 658: 659: 660: 661: 662: 663: 664: 665: 666: 667: 668: 669: 670: 671: 672: 673: 674: 675: 676: 677: 678: 679: 680: 681: 682: 683: 684: 685: 686: 687: 688: 689: 690: 691: 692: 693: 694: 695: 696: 697: 698: 699: 700: 701: 702: 703: 704: 705: 706: 707: 708: 709: 710: 711: 712: 713: 714: 715: 716: 717: 718: 719: 720: 721: 722: 723: 724: 725: 726: 727: 728: 729: 730: 731: 732: 733: 734: 735: 736: 737: 738: 739: 740: 741: 742: 743: 744: 745: 746: 747: 748: 749: 750: 751: 752: 753: 754: 755: 756: 757: 758: 759: 760: 761: 762: 763: 764: 765: 766: 767: 768: 769: 770: 771: 772: 773: 774: 775: 776: 777: 778: 779: 780: 781: 782: 783: 784: 785: 786: 787: 788: 789: 790: 791: 792: 793: 794: 795: 796: 797: 798: 799: 800: 801: 802: 803: 804: 805: 806: 807: 808: 809: 810: 811: 812: 813: 814: 815: 816: 817: 818: 819: 820: 821: 822: 823: 824: 825: 826: 827: 828: 829: 830: 831: 832: 833: 834: 835: 836: 837: 838: 839: 840: 841: 842: 843: 844: 845: 846: 847: 848: 849: 850: 851: 852: 853: 854: 855: 856: 857: 858: 859: 860: 861: 862: 863: 864: 865: 866: 867: 868: 869: 870: 871: 872: 873: 874: 875: 876: 877: 878: 879: 880: 881: 882: 883: 884: 885: 886: 887: 888: 889: 890: 891: 892: 893: 894: 895: 896: 897: 898: 899: 900: 901: 902: 903: 904: 905: 906: 907: 908: 909: 910: 911: 912: 913: 914: 915: 916: 917: 918: 919: 920: 921: 922: 923: 924: 925: 926: 927: 928: 929: 930: 931: 932: 933: 934: 935: 936: 937: 938: 939: 940: 941: 942: 943: 944: 945: 946: 947: 948: 949: 950: 951: 952: 953: 954: 955: 956: 957: 958: 959: 960: 961: 962: 963: 964: 965: 966: 967: 968: 969: 970: 971: 972: 973: 974: 975: 976: 977: 978: 979: 980: 981: 982: 983: 984: 985: 986: 987: 988: 989: 990: 991: 992: 993: 994: 995: 996: 997: 998: 999: 1000: 1001: 1002: 1003: 1004: 1005: 1006: 1007: 1008: 1009: 1010: 1011: 1012: 1013: 1014: 1015: 1016: 1017: 1018: 1019: 1020: 1021: 1022: 1023: 1024: 1025: 1026: 1027:
+
<?php
+
+ namespace splitbrain\PHPArchive;
+
+
+ class Zip extends Archive
+{
+ const LOCAL_FILE_HEADER_CRC_OFFSET = 14 ;
+
+ protected $file = '' ;
+ protected $fh ;
+ protected $memory = '' ;
+ protected $closed = true ;
+ protected $writeaccess = false ;
+ protected $ctrl_dir ;
+ protected $complevel = 9 ;
+
+
+ public function setCompression($level = 9 , $type = Archive::COMPRESS_AUTO)
+ {
+ if ($level < -1 || $level > 9 ) {
+ throw new ArchiveIllegalCompressionException('Compression level should be between -1 and 9' );
+ }
+ $this ->complevel = $level ;
+ }
+
+
+ public function open($file )
+ {
+ $this ->file = $file ;
+ $this ->fh = @fopen ($this ->file , 'rb' );
+ if (!$this ->fh) {
+ throw new ArchiveIOException('Could not open file for reading: ' .$this ->file );
+ }
+ $this ->closed = false ;
+ }
+
+
+ public function contents()
+ {
+ $result = array ();
+
+ foreach ($this ->yieldContents() as $fileinfo ) {
+ $result [] = $fileinfo ;
+ }
+
+ return $result ;
+ }
+
+
+ public function yieldContents()
+ {
+ if ($this ->closed || !$this ->file ) {
+ throw new ArchiveIOException('Can not read from a closed archive' );
+ }
+
+ $centd = $this ->readCentralDir();
+
+ @rewind ($this ->fh);
+ @fseek ($this ->fh, $centd ['offset' ]);
+
+ for ($i = 0 ; $i < $centd ['entries' ]; $i ++) {
+ yield $this ->header2fileinfo($this ->readCentralFileHeader());
+ }
+
+ $this ->close();
+ }
+
+
+ public function extract ($outdir , $strip = '' , $exclude = '' , $include = '' )
+ {
+ if ($this ->closed || !$this ->file ) {
+ throw new ArchiveIOException('Can not read from a closed archive' );
+ }
+
+ $outdir = rtrim ($outdir , '/' );
+ @mkdir ($outdir , 0777 , true );
+
+ $extracted = array ();
+
+ $cdir = $this ->readCentralDir();
+ $pos_entry = $cdir ['offset' ];
+
+ for ($i = 0 ; $i < $cdir ['entries' ]; $i ++) {
+
+ @fseek ($this ->fh, $pos_entry );
+ $header = $this ->readCentralFileHeader();
+ $header ['index' ] = $i ;
+ $pos_entry = ftell ($this ->fh);
+ fseek ($this ->fh, $header ['offset' ]);
+ $header = $this ->readFileHeader($header );
+ $fileinfo = $this ->header2fileinfo($header );
+
+
+ $fileinfo ->strip($strip );
+
+
+ if (!strlen ($fileinfo ->getPath()) || !$fileinfo ->matchExpression($include , $exclude )) {
+ continue ;
+ }
+
+ $extracted [] = $fileinfo ;
+
+
+ $output = $outdir .'/' .$fileinfo ->getPath();
+ $directory = ($header ['folder' ]) ? $output : dirname ($output );
+ @mkdir ($directory , 0777 , true );
+
+
+ if ($fileinfo ->getIsdir()) {
+ if (is_callable ($this ->callback)) {
+ call_user_func ($this ->callback, $fileinfo );
+ }
+ continue ;
+ }
+
+
+ if ($header ['compression' ] == 0 ) {
+ $extractto = $output ;
+ } else {
+ $extractto = $output .'.gz' ;
+ }
+
+
+ $fp = @fopen ($extractto , "wb" );
+ if (!$fp ) {
+ throw new ArchiveIOException('Could not open file for writing: ' .$extractto );
+ }
+
+
+ if ($header ['compression' ] != 0 ) {
+ $binary_data = pack (
+ 'va1a1Va1a1' ,
+ 0x8b1f ,
+ chr ($header ['compression' ]),
+ chr (0x00 ),
+ time (),
+ chr (0x00 ),
+ chr (3 )
+ );
+ fwrite ($fp , $binary_data , 10 );
+ }
+
+
+ $size = $header ['compressed_size' ];
+ while ($size != 0 ) {
+ $read_size = ($size < 2048 ? $size : 2048 );
+ $buffer = fread ($this ->fh, $read_size );
+ $binary_data = pack ('a' .$read_size , $buffer );
+ fwrite ($fp , $binary_data , $read_size );
+ $size -= $read_size ;
+ }
+
+
+ if ($header ['compression' ] != 0 ) {
+ $binary_data = pack ('VV' , $header ['crc' ], $header ['size' ]);
+ fwrite ($fp , $binary_data , 8 );
+ }
+
+
+ fclose ($fp );
+
+
+ if ($header ['compression' ] != 0 ) {
+ $gzp = @gzopen ($extractto , 'rb' );
+ if (!$gzp ) {
+ @unlink ($extractto );
+ throw new ArchiveIOException('Failed file extracting. gzip support missing?' );
+ }
+ $fp = @fopen ($output , 'wb' );
+ if (!$fp ) {
+ throw new ArchiveIOException('Could not open file for writing: ' .$extractto );
+ }
+
+ $size = $header ['size' ];
+ while ($size != 0 ) {
+ $read_size = ($size < 2048 ? $size : 2048 );
+ $buffer = gzread ($gzp , $read_size );
+ $binary_data = pack ('a' .$read_size , $buffer );
+ @fwrite ($fp , $binary_data , $read_size );
+ $size -= $read_size ;
+ }
+ fclose ($fp );
+ gzclose ($gzp );
+ unlink ($extractto );
+ }
+
+ @touch ($output , $fileinfo ->getMtime());
+
+ if (is_callable ($this ->callback)) {
+ call_user_func ($this ->callback, $fileinfo );
+ }
+ }
+
+ $this ->close();
+ return $extracted ;
+ }
+
+
+ public function create($file = '' )
+ {
+ $this ->file = $file ;
+ $this ->memory = '' ;
+ $this ->fh = 0 ;
+
+ if ($this ->file ) {
+ $this ->fh = @fopen ($this ->file , 'wb' );
+
+ if (!$this ->fh) {
+ throw new ArchiveIOException('Could not open file for writing: ' .$this ->file );
+ }
+ }
+ $this ->writeaccess = true ;
+ $this ->closed = false ;
+ $this ->ctrl_dir = array ();
+ }
+
+
+
+
+ public function addFile($file , $fileinfo = '' )
+ {
+ if (is_string ($fileinfo )) {
+ $fileinfo = FileInfo::fromPath($file , $fileinfo );
+ }
+
+ if ($this ->closed) {
+ throw new ArchiveIOException('Archive has been closed, files can no longer be added' );
+ }
+
+ $fp = @fopen ($file , 'rb' );
+ if ($fp === false ) {
+ throw new ArchiveIOException('Could not open file for reading: ' .$file );
+ }
+
+ $offset = $this ->dataOffset();
+ $name = $fileinfo ->getPath();
+ $time = $fileinfo ->getMtime();
+
+
+ $this ->writebytes($this ->makeLocalFileHeader(
+ $time ,
+ 0 ,
+ 0 ,
+ 0 ,
+ $name ,
+ (bool) $this ->complevel
+ ));
+
+
+
+
+ $deflate_context = deflate_init(ZLIB_ENCODING_DEFLATE, ['level' => $this ->complevel]);
+ $crc_context = hash_init ('crc32b' );
+ $size = $csize = 0 ;
+
+ while (!feof ($fp )) {
+ $block = fread ($fp , 512 );
+
+ if ($this ->complevel) {
+ $is_first_block = $size === 0 ;
+ $is_last_block = feof ($fp );
+
+ if ($is_last_block ) {
+ $c_block = deflate_add($deflate_context , $block , ZLIB_FINISH);
+
+ $c_block = substr ($c_block , 0 , -4 );
+ } else {
+ $c_block = deflate_add($deflate_context , $block , ZLIB_NO_FLUSH);
+ }
+
+
+ if ($is_first_block ) {
+ $c_block = substr ($c_block , 2 );
+ }
+
+ $csize += strlen ($c_block );
+ $this ->writebytes($c_block );
+ } else {
+ $this ->writebytes($block );
+ }
+
+ $size += strlen ($block );
+ hash_update ($crc_context , $block );
+ }
+ fclose ($fp );
+
+
+ $crc = hexdec (hash_final ($crc_context ));
+ $csize = $this ->complevel ? $csize : $size ;
+ $this ->writebytesAt($this ->makeCrcAndSize(
+ $crc ,
+ $size ,
+ $csize
+ ), $offset + self::LOCAL_FILE_HEADER_CRC_OFFSET);
+
+
+
+
+ $this ->ctrl_dir[] = $this ->makeCentralFileRecord(
+ $offset ,
+ $time ,
+ $crc ,
+ $size ,
+ $csize ,
+ $name ,
+ (bool) $this ->complevel
+ );
+
+ if (is_callable ($this ->callback)) {
+ call_user_func ($this ->callback, $fileinfo );
+ }
+ }
+
+
+ public function addData($fileinfo , $data )
+ {
+ if (is_string ($fileinfo )) {
+ $fileinfo = new FileInfo($fileinfo );
+ }
+
+ if ($this ->closed) {
+ throw new ArchiveIOException('Archive has been closed, files can no longer be added' );
+ }
+
+
+ $size = strlen ($data );
+ $crc = crc32 ($data );
+ if ($this ->complevel) {
+ $data = gzcompress ($data , $this ->complevel);
+ $data = substr ($data , 2 , -4 );
+ }
+ $csize = strlen ($data );
+ $offset = $this ->dataOffset();
+ $name = $fileinfo ->getPath();
+ $time = $fileinfo ->getMtime();
+
+
+ $this ->writebytes($this ->makeLocalFileHeader(
+ $time ,
+ $crc ,
+ $size ,
+ $csize ,
+ $name ,
+ (bool) $this ->complevel
+ ));
+
+
+
+
+ $this ->writebytes($data );
+
+
+
+
+ $this ->ctrl_dir[] = $this ->makeCentralFileRecord(
+ $offset ,
+ $time ,
+ $crc ,
+ $size ,
+ $csize ,
+ $name ,
+ (bool) $this ->complevel
+ );
+
+ if (is_callable ($this ->callback)) {
+ call_user_func ($this ->callback, $fileinfo );
+ }
+ }
+
+
+ public function close()
+ {
+ if ($this ->closed) {
+ return ;
+ }
+
+ if ($this ->writeaccess) {
+
+ $offset = $this ->dataOffset();
+ $ctrldir = join ('' , $this ->ctrl_dir);
+ $this ->writebytes($ctrldir );
+
+
+ $this ->writebytes("\x50\x4b\x05\x06" );
+ $this ->writebytes(pack ('v' , 0 ));
+ $this ->writebytes(pack ('v' , 0 ));
+ $this ->writebytes(pack ('v' ,
+ count ($this ->ctrl_dir)));
+ $this ->writebytes(pack ('v' , count ($this ->ctrl_dir)));
+ $this ->writebytes(pack ('V' , strlen ($ctrldir )));
+ $this ->writebytes(pack ('V' ,
+ $offset ));
+ $this ->writebytes(pack ('v' , 0 ));
+
+ $this ->ctrl_dir = array ();
+ }
+
+
+ if ($this ->file ) {
+ fclose ($this ->fh);
+ $this ->file = '' ;
+ $this ->fh = 0 ;
+ }
+
+ $this ->writeaccess = false ;
+ $this ->closed = true ;
+ }
+
+
+ public function getArchive()
+ {
+ $this ->close();
+
+ return $this ->memory;
+ }
+
+
+ public function save($file )
+ {
+ if (!@file_put_contents ($file , $this ->getArchive())) {
+ throw new ArchiveIOException('Could not write to file: ' .$file );
+ }
+ }
+
+
+ protected function readCentralDir()
+ {
+ $size = filesize ($this ->file );
+ if ($size < 277 ) {
+ $maximum_size = $size ;
+ } else {
+ $maximum_size = 277 ;
+ }
+
+ @fseek ($this ->fh, $size - $maximum_size );
+ $pos = ftell ($this ->fh);
+ $bytes = 0x00000000 ;
+
+ while ($pos < $size ) {
+ $byte = @fread ($this ->fh, 1 );
+ $bytes = (($bytes << 8 ) & 0xFFFFFFFF ) | ord ($byte );
+ if ($bytes == 0x504b0506 ) {
+ break ;
+ }
+ $pos ++;
+ }
+
+ $data = unpack (
+ 'vdisk/vdisk_start/vdisk_entries/ventries/Vsize/Voffset/vcomment_size' ,
+ fread ($this ->fh, 18 )
+ );
+
+ if ($data ['comment_size' ] != 0 ) {
+ $centd ['comment' ] = fread ($this ->fh, $data ['comment_size' ]);
+ } else {
+ $centd ['comment' ] = '' ;
+ }
+ $centd ['entries' ] = $data ['entries' ];
+ $centd ['disk_entries' ] = $data ['disk_entries' ];
+ $centd ['offset' ] = $data ['offset' ];
+ $centd ['disk_start' ] = $data ['disk_start' ];
+ $centd ['size' ] = $data ['size' ];
+ $centd ['disk' ] = $data ['disk' ];
+ return $centd ;
+ }
+
+
+ protected function readCentralFileHeader()
+ {
+ $binary_data = fread ($this ->fh, 46 );
+ $header = unpack (
+ 'vchkid/vid/vversion/vversion_extracted/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len/vcomment_len/vdisk/vinternal/Vexternal/Voffset' ,
+ $binary_data
+ );
+
+ if ($header ['filename_len' ] != 0 ) {
+ $header ['filename' ] = fread ($this ->fh, $header ['filename_len' ]);
+ } else {
+ $header ['filename' ] = '' ;
+ }
+
+ if ($header ['extra_len' ] != 0 ) {
+ $header ['extra' ] = fread ($this ->fh, $header ['extra_len' ]);
+ $header ['extradata' ] = $this ->parseExtra($header ['extra' ]);
+ } else {
+ $header ['extra' ] = '' ;
+ $header ['extradata' ] = array ();
+ }
+
+ if ($header ['comment_len' ] != 0 ) {
+ $header ['comment' ] = fread ($this ->fh, $header ['comment_len' ]);
+ } else {
+ $header ['comment' ] = '' ;
+ }
+
+ $header ['mtime' ] = $this ->makeUnixTime($header ['mdate' ], $header ['mtime' ]);
+ $header ['stored_filename' ] = $header ['filename' ];
+ $header ['status' ] = 'ok' ;
+ if (substr ($header ['filename' ], -1 ) == '/' ) {
+ $header ['external' ] = 0x41FF0010 ;
+ }
+ $header ['folder' ] = ($header ['external' ] == 0x41FF0010 || $header ['external' ] == 16 ) ? 1 : 0 ;
+
+ return $header ;
+ }
+
+
+ protected function readFileHeader($header )
+ {
+ $binary_data = fread ($this ->fh, 30 );
+ $data = unpack (
+ 'vchk/vid/vversion/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len' ,
+ $binary_data
+ );
+
+ $header ['filename' ] = fread ($this ->fh, $data ['filename_len' ]);
+ if ($data ['extra_len' ] != 0 ) {
+ $header ['extra' ] = fread ($this ->fh, $data ['extra_len' ]);
+ $header ['extradata' ] = array_merge ($header ['extradata' ], $this ->parseExtra($header ['extra' ]));
+ } else {
+ $header ['extra' ] = '' ;
+ $header ['extradata' ] = array ();
+ }
+
+ $header ['compression' ] = $data ['compression' ];
+ foreach (array (
+ 'size' ,
+ 'compressed_size' ,
+ 'crc'
+ ) as $hd ) {
+ if ($data [$hd ] != 0 ) {
+ $header [$hd ] = $data [$hd ];
+ }
+ }
+ $header ['flag' ] = $data ['flag' ];
+ $header ['mtime' ] = $this ->makeUnixTime($data ['mdate' ], $data ['mtime' ]);
+
+ $header ['stored_filename' ] = $header ['filename' ];
+ $header ['status' ] = "ok" ;
+ $header ['folder' ] = ($header ['external' ] == 0x41FF0010 || $header ['external' ] == 16 ) ? 1 : 0 ;
+ return $header ;
+ }
+
+
+ protected function parseExtra($header )
+ {
+ $extra = array ();
+
+ while (strlen ($header ) !== 0 ) {
+ $set = unpack ('vid/vlen' , $header );
+ $header = substr ($header , 4 );
+ $value = substr ($header , 0 , $set ['len' ]);
+ $header = substr ($header , $set ['len' ]);
+ $extra [$set ['id' ]] = $value ;
+ }
+
+
+ if (isset ($extra [0x6375 ])) {
+ $extra ['utf8comment' ] = substr ($extra [0x7075 ], 5 );
+ }
+ if (isset ($extra [0x7075 ])) {
+ $extra ['utf8path' ] = substr ($extra [0x7075 ], 5 );
+ }
+
+ return $extra ;
+ }
+
+
+ protected function header2fileinfo($header )
+ {
+ $fileinfo = new FileInfo();
+ $fileinfo ->setSize($header ['size' ]);
+ $fileinfo ->setCompressedSize($header ['compressed_size' ]);
+ $fileinfo ->setMtime($header ['mtime' ]);
+ $fileinfo ->setComment($header ['comment' ]);
+ $fileinfo ->setIsdir($header ['external' ] == 0x41FF0010 || $header ['external' ] == 16 );
+
+ if (isset ($header ['extradata' ]['utf8path' ])) {
+ $fileinfo ->setPath($header ['extradata' ]['utf8path' ]);
+ } else {
+ $fileinfo ->setPath($this ->cpToUtf8($header ['filename' ]));
+ }
+
+ if (isset ($header ['extradata' ]['utf8comment' ])) {
+ $fileinfo ->setComment($header ['extradata' ]['utf8comment' ]);
+ } else {
+ $fileinfo ->setComment($this ->cpToUtf8($header ['comment' ]));
+ }
+
+ return $fileinfo ;
+ }
+
+
+ protected function cpToUtf8($string )
+ {
+ if (function_exists ('iconv' ) && @iconv_strlen ('' , 'CP437' ) !== false ) {
+ return iconv ('CP437' , 'UTF-8' , $string );
+ } elseif (function_exists ('mb_convert_encoding' )) {
+ return mb_convert_encoding ($string , 'UTF-8' , 'CP850' );
+ } else {
+ return $string ;
+ }
+ }
+
+
+ protected function utf8ToCp($string )
+ {
+
+ if (function_exists ('iconv' )) {
+ $conv = @iconv ('UTF-8' , 'CP437//IGNORE' , $string );
+ if ($conv ) return $conv ;
+ }
+
+
+
+
+ if (function_exists ('mb_convert_encoding' )) {
+ return mb_convert_encoding ($string , 'CP850' , 'UTF-8' );
+ } else {
+ return $string ;
+ }
+ }
+
+
+
+ protected function writebytes($data )
+ {
+ if (!$this ->file ) {
+ $this ->memory .= $data ;
+ $written = strlen ($data );
+ } else {
+ $written = @fwrite ($this ->fh, $data );
+ }
+ if ($written === false ) {
+ throw new ArchiveIOException('Failed to write to archive stream' );
+ }
+ return $written ;
+ }
+
+
+ protected function writebytesAt($data , $offset ) {
+ if (!$this ->file ) {
+ $this ->memory .= substr_replace ($this ->memory, $data , $offset );
+ $written = strlen ($data );
+ } else {
+ @fseek ($this ->fh, $offset );
+ $written = @fwrite ($this ->fh, $data );
+ @fseek ($this ->fh, 0 , SEEK_END);
+ }
+ if ($written === false ) {
+ throw new ArchiveIOException('Failed to write to archive stream' );
+ }
+ return $written ;
+ }
+
+
+ protected function dataOffset()
+ {
+ if ($this ->file ) {
+ return ftell ($this ->fh);
+ } else {
+ return strlen ($this ->memory);
+ }
+ }
+
+
+ protected function makeDosTime($time )
+ {
+ $timearray = getdate ($time );
+ if ($timearray ['year' ] < 1980 ) {
+ $timearray ['year' ] = 1980 ;
+ $timearray ['mon' ] = 1 ;
+ $timearray ['mday' ] = 1 ;
+ $timearray ['hours' ] = 0 ;
+ $timearray ['minutes' ] = 0 ;
+ $timearray ['seconds' ] = 0 ;
+ }
+ return (($timearray ['year' ] - 1980 ) << 25 ) |
+ ($timearray ['mon' ] << 21 ) |
+ ($timearray ['mday' ] << 16 ) |
+ ($timearray ['hours' ] << 11 ) |
+ ($timearray ['minutes' ] << 5 ) |
+ ($timearray ['seconds' ] >> 1 );
+ }
+
+
+ protected function makeUnixTime($mdate = null , $mtime = null )
+ {
+ if ($mdate && $mtime ) {
+ $year = (($mdate & 0xFE00 ) >> 9 ) + 1980 ;
+ $month = ($mdate & 0x01E0 ) >> 5 ;
+ $day = $mdate & 0x001F ;
+
+ $hour = ($mtime & 0xF800 ) >> 11 ;
+ $minute = ($mtime & 0x07E0 ) >> 5 ;
+ $seconde = ($mtime & 0x001F ) << 1 ;
+
+ $mtime = mktime ($hour , $minute , $seconde , $month , $day , $year );
+ } else {
+ $mtime = time ();
+ }
+
+ return $mtime ;
+ }
+
+
+ protected function makeCentralFileRecord($offset , $ts , $crc , $len , $clen , $name , $comp = null )
+ {
+ if (is_null ($comp )) $comp = $len != $clen ;
+ $comp = $comp ? 8 : 0 ;
+ $dtime = dechex ($this ->makeDosTime($ts ));
+
+ list ($name , $extra ) = $this ->encodeFilename($name );
+
+ $header = "\x50\x4b\x01\x02" ;
+ $header .= pack ('v' , 14 );
+ $header .= pack ('v' , 20 );
+ $header .= pack ('v' , 0 );
+ $header .= pack ('v' , $comp );
+ $header .= pack (
+ 'H*' ,
+ $dtime [6 ] . $dtime [7 ] .
+ $dtime [4 ] . $dtime [5 ] .
+ $dtime [2 ] . $dtime [3 ] .
+ $dtime [0 ] . $dtime [1 ]
+ );
+ $header .= pack ('V' , $crc );
+ $header .= pack ('V' , $clen );
+ $header .= pack ('V' , $len );
+ $header .= pack ('v' , strlen ($name ));
+ $header .= pack ('v' , strlen ($extra ));
+ $header .= pack ('v' , 0 );
+ $header .= pack ('v' , 0 );
+ $header .= pack ('v' , 0 );
+ $header .= pack ('V' , 0 );
+ $header .= pack ('V' , $offset );
+ $header .= $name ;
+ $header .= $extra ;
+
+ return $header ;
+ }
+
+
+ protected function makeLocalFileHeader($ts , $crc , $len , $clen , $name , $comp = null )
+ {
+ if (is_null ($comp )) $comp = $len != $clen ;
+ $comp = $comp ? 8 : 0 ;
+ $dtime = dechex ($this ->makeDosTime($ts ));
+
+ list ($name , $extra ) = $this ->encodeFilename($name );
+
+ $header = "\x50\x4b\x03\x04" ;
+ $header .= pack ('v' , 20 );
+ $header .= pack ('v' , 0 );
+ $header .= pack ('v' , $comp );
+ $header .= pack (
+ 'H*' ,
+ $dtime [6 ] . $dtime [7 ] .
+ $dtime [4 ] . $dtime [5 ] .
+ $dtime [2 ] . $dtime [3 ] .
+ $dtime [0 ] . $dtime [1 ]
+ );
+ $header .= pack ('V' , $crc );
+ $header .= pack ('V' , $clen );
+ $header .= pack ('V' , $len );
+ $header .= pack ('v' , strlen ($name ));
+ $header .= pack ('v' , strlen ($extra ));
+ $header .= $name ;
+ $header .= $extra ;
+ return $header ;
+ }
+
+
+ protected function makeCrcAndSize($crc , $len , $clen ) {
+ $header = pack ('V' , $crc );
+ $header .= pack ('V' , $clen );
+ $header .= pack ('V' , $len );
+ return $header ;
+ }
+
+
+ protected function encodeFilename($original )
+ {
+ $cp437 = $this ->utf8ToCp($original );
+ if ($cp437 === $original ) {
+ return array ($original , '' );
+ }
+
+ $extra = pack (
+ 'vvCV' ,
+ 0x7075 ,
+ strlen ($original ) + 5 ,
+ 1 ,
+ crc32 ($original )
+ );
+ $extra .= $original ;
+
+ return array ($cp437 , $extra );
+ }
+ }
+
+
+
+
+
+
+
+
+
diff --git a/src/Archive.php b/src/Archive.php
deleted file mode 100644
index 45c87fd..0000000
--- a/src/Archive.php
+++ /dev/null
@@ -1,135 +0,0 @@
-callback = $callback;
- }
-}
diff --git a/src/ArchiveCorruptedException.php b/src/ArchiveCorruptedException.php
deleted file mode 100644
index a87cff4..0000000
--- a/src/ArchiveCorruptedException.php
+++ /dev/null
@@ -1,10 +0,0 @@
-
- * @package splitbrain\PHPArchive
- * @license MIT
- */
-class FileInfo
-{
-
- protected $isdir = false;
- protected $path = '';
- protected $size = 0;
- protected $csize = 0;
- protected $mtime = 0;
- protected $mode = 0664;
- protected $owner = '';
- protected $group = '';
- protected $uid = 0;
- protected $gid = 0;
- protected $comment = '';
-
- /**
- * initialize dynamic defaults
- *
- * @param string $path The path of the file, can also be set later through setPath()
- */
- public function __construct($path = '')
- {
- $this->mtime = time();
- $this->setPath($path);
- }
-
- /**
- * Handle calls to deprecated methods
- *
- * @param string $name
- * @param array $arguments
- * @return mixed
- */
- public function __call($name, $arguments)
- {
- if($name === 'match') {
- trigger_error('FileInfo::match() is deprecated, use FileInfo::matchExpression() instead.', E_USER_NOTICE);
- return call_user_func_array([$this, $name], $arguments);
- }
-
- trigger_error('Call to undefined method FileInfo::'.$name.'()', E_USER_ERROR);
- return null;
- }
-
- /**
- * Factory to build FileInfo from existing file or directory
- *
- * @param string $path path to a file on the local file system
- * @param string $as optional path to use inside the archive
- * @throws FileInfoException
- * @return FileInfo
- */
- public static function fromPath($path, $as = '')
- {
- clearstatcache(false, $path);
-
- if (!file_exists($path)) {
- throw new FileInfoException("$path does not exist");
- }
-
- $stat = stat($path);
- $file = new FileInfo();
-
- $file->setPath($path);
- $file->setIsdir(is_dir($path));
- $file->setMode(fileperms($path));
- $file->setOwner(fileowner($path));
- $file->setGroup(filegroup($path));
- $file->setSize(filesize($path));
- $file->setUid($stat['uid']);
- $file->setGid($stat['gid']);
- $file->setMtime($stat['mtime']);
-
- if ($as) {
- $file->setPath($as);
- }
-
- return $file;
- }
-
- /**
- * @return int the filesize. always 0 for directories
- */
- public function getSize()
- {
- if($this->isdir) return 0;
- return $this->size;
- }
-
- /**
- * @param int $size
- */
- public function setSize($size)
- {
- $this->size = $size;
- }
-
- /**
- * @return int
- */
- public function getCompressedSize()
- {
- return $this->csize;
- }
-
- /**
- * @param int $csize
- */
- public function setCompressedSize($csize)
- {
- $this->csize = $csize;
- }
-
- /**
- * @return int
- */
- public function getMtime()
- {
- return $this->mtime;
- }
-
- /**
- * @param int $mtime
- */
- public function setMtime($mtime)
- {
- $this->mtime = $mtime;
- }
-
- /**
- * @return int
- */
- public function getGid()
- {
- return $this->gid;
- }
-
- /**
- * @param int $gid
- */
- public function setGid($gid)
- {
- $this->gid = $gid;
- }
-
- /**
- * @return int
- */
- public function getUid()
- {
- return $this->uid;
- }
-
- /**
- * @param int $uid
- */
- public function setUid($uid)
- {
- $this->uid = $uid;
- }
-
- /**
- * @return string
- */
- public function getComment()
- {
- return $this->comment;
- }
-
- /**
- * @param string $comment
- */
- public function setComment($comment)
- {
- $this->comment = $comment;
- }
-
- /**
- * @return string
- */
- public function getGroup()
- {
- return $this->group;
- }
-
- /**
- * @param string $group
- */
- public function setGroup($group)
- {
- $this->group = $group;
- }
-
- /**
- * @return boolean
- */
- public function getIsdir()
- {
- return $this->isdir;
- }
-
- /**
- * @param boolean $isdir
- */
- public function setIsdir($isdir)
- {
- // default mode for directories
- if ($isdir && $this->mode === 0664) {
- $this->mode = 0775;
- }
- $this->isdir = $isdir;
- }
-
- /**
- * @return int
- */
- public function getMode()
- {
- return $this->mode;
- }
-
- /**
- * @param int $mode
- */
- public function setMode($mode)
- {
- $this->mode = $mode;
- }
-
- /**
- * @return string
- */
- public function getOwner()
- {
- return $this->owner;
- }
-
- /**
- * @param string $owner
- */
- public function setOwner($owner)
- {
- $this->owner = $owner;
- }
-
- /**
- * @return string
- */
- public function getPath()
- {
- return $this->path;
- }
-
- /**
- * @param string $path
- */
- public function setPath($path)
- {
- $this->path = $this->cleanPath($path);
- }
-
- /**
- * Cleans up a path and removes relative parts, also strips leading slashes
- *
- * @param string $path
- * @return string
- */
- protected function cleanPath($path)
- {
- $path = str_replace('\\', '/', $path);
- $path = explode('/', $path);
- $newpath = array();
- foreach ($path as $p) {
- if ($p === '' || $p === '.') {
- continue;
- }
- if ($p === '..') {
- array_pop($newpath);
- continue;
- }
- array_push($newpath, $p);
- }
- return trim(implode('/', $newpath), '/');
- }
-
- /**
- * Strip given prefix or number of path segments from the filename
- *
- * The $strip parameter allows you to strip a certain number of path components from the filenames
- * found in the tar file, similar to the --strip-components feature of GNU tar. This is triggered when
- * an integer is passed as $strip.
- * Alternatively a fixed string prefix may be passed in $strip. If the filename matches this prefix,
- * the prefix will be stripped. It is recommended to give prefixes with a trailing slash.
- *
- * @param int|string $strip
- */
- public function strip($strip)
- {
- $filename = $this->getPath();
- $striplen = strlen($strip);
- if (is_int($strip)) {
- // if $strip is an integer we strip this many path components
- $parts = explode('/', $filename);
- if (!$this->getIsdir()) {
- $base = array_pop($parts); // keep filename itself
- } else {
- $base = '';
- }
- $filename = join('/', array_slice($parts, $strip));
- if ($base) {
- $filename .= "/$base";
- }
- } else {
- // if strip is a string, we strip a prefix here
- if (substr($filename, 0, $striplen) == $strip) {
- $filename = substr($filename, $striplen);
- }
- }
-
- $this->setPath($filename);
- }
-
- /**
- * Does the file match the given include and exclude expressions?
- *
- * Exclude rules take precedence over include rules
- *
- * @param string $include Regular expression of files to include
- * @param string $exclude Regular expression of files to exclude
- * @return bool
- */
- public function matchExpression($include = '', $exclude = '')
- {
- $extract = true;
- if ($include && !preg_match($include, $this->getPath())) {
- $extract = false;
- }
- if ($exclude && preg_match($exclude, $this->getPath())) {
- $extract = false;
- }
-
- return $extract;
- }
-}
-
diff --git a/src/FileInfoException.php b/src/FileInfoException.php
deleted file mode 100644
index 9c6acaa..0000000
--- a/src/FileInfoException.php
+++ /dev/null
@@ -1,10 +0,0 @@
-100 chars) are supported in POSIX ustar and GNU longlink formats.
- *
- * @author Andreas Gohr
- * @package splitbrain\PHPArchive
- * @license MIT
- */
-class Tar extends Archive
-{
- const READ_CHUNK_SIZE = 1048576; // 1MB
-
- protected $file = '';
- protected $comptype = Archive::COMPRESS_AUTO;
- protected $complevel = 9;
- protected $fh;
- protected $memory = '';
- protected $closed = true;
- protected $writeaccess = false;
- protected $position = 0;
- protected $contentUntil = 0;
- protected $skipUntil = 0;
-
- /**
- * Sets the compression to use
- *
- * @param int $level Compression level (0 to 9)
- * @param int $type Type of compression to use (use COMPRESS_* constants)
- * @throws ArchiveIllegalCompressionException
- */
- public function setCompression($level = 9, $type = Archive::COMPRESS_AUTO)
- {
- $this->compressioncheck($type);
- if ($level < -1 || $level > 9) {
- throw new ArchiveIllegalCompressionException('Compression level should be between -1 and 9');
- }
- $this->comptype = $type;
- $this->complevel = $level;
- if($level == 0) $this->comptype = Archive::COMPRESS_NONE;
- if($type == Archive::COMPRESS_NONE) $this->complevel = 0;
- }
-
- /**
- * Open an existing TAR file for reading
- *
- * @param string $file
- * @throws ArchiveIOException
- * @throws ArchiveIllegalCompressionException
- */
- public function open($file)
- {
- $this->file = $file;
-
- // update compression to mach file
- if ($this->comptype == Tar::COMPRESS_AUTO) {
- $this->setCompression($this->complevel, $this->filetype($file));
- }
-
- // open file handles
- if ($this->comptype === Archive::COMPRESS_GZIP) {
- $this->fh = @gzopen($this->file, 'rb');
- } elseif ($this->comptype === Archive::COMPRESS_BZIP) {
- $this->fh = @bzopen($this->file, 'r');
- } else {
- $this->fh = @fopen($this->file, 'rb');
- }
-
- if (!$this->fh) {
- throw new ArchiveIOException('Could not open file for reading: '.$this->file);
- }
- $this->closed = false;
- $this->position = 0;
- }
-
- /**
- * Read the contents of a TAR archive
- *
- * This function lists the files stored in the archive
- *
- * The archive is closed afer reading the contents, because rewinding is not possible in bzip2 streams.
- * Reopen the file with open() again if you want to do additional operations
- *
- * @throws ArchiveIOException
- * @throws ArchiveCorruptedException
- * @returns FileInfo[]
- */
- public function contents()
- {
- $result = array();
-
- foreach ($this->yieldContents() as $fileinfo) {
- $result[] = $fileinfo;
- }
-
- return $result;
- }
-
- /**
- * Read the contents of a TAR archive and return each entry using yield
- * for memory efficiency.
- *
- * @see contents()
- * @throws ArchiveIOException
- * @throws ArchiveCorruptedException
- * @returns FileInfo[]
- */
- public function yieldContents()
- {
- if ($this->closed || !$this->file) {
- throw new ArchiveIOException('Can not read from a closed archive');
- }
-
- while ($read = $this->readbytes(512)) {
- $header = $this->parseHeader($read);
- if (!is_array($header)) {
- continue;
- }
-
- $this->contentUntil = $this->position + $header['size'];
- $this->skipUntil = $this->position + ceil($header['size'] / 512) * 512;
-
- yield $this->header2fileinfo($header);
-
- $skip = $this->skipUntil - $this->position;
- if ($skip > 0) {
- $this->skipbytes($skip);
- }
- }
-
- $this->close();
- }
-
- /**
- * Reads content of a current archive entry.
- *
- * Works only when iterating trough the archive using the generator returned
- * by the yieldContents().
- *
- * @param int $length maximum number of bytes to read
- *
- * @return string
- */
- public function readCurrentEntry($length = PHP_INT_MAX)
- {
- $length = (int) min($length, $this->contentUntil - $this->position);
- if ($length === 0) {
- return '';
- }
- return $this->readbytes($length);
- }
-
- /**
- * Extract an existing TAR archive
- *
- * The $strip parameter allows you to strip a certain number of path components from the filenames
- * found in the tar file, similar to the --strip-components feature of GNU tar. This is triggered when
- * an integer is passed as $strip.
- * Alternatively a fixed string prefix may be passed in $strip. If the filename matches this prefix,
- * the prefix will be stripped. It is recommended to give prefixes with a trailing slash.
- *
- * By default this will extract all files found in the archive. You can restrict the output using the $include
- * and $exclude parameter. Both expect a full regular expression (including delimiters and modifiers). If
- * $include is set only files that match this expression will be extracted. Files that match the $exclude
- * expression will never be extracted. Both parameters can be used in combination. Expressions are matched against
- * stripped filenames as described above.
- *
- * The archive is closed afer reading the contents, because rewinding is not possible in bzip2 streams.
- * Reopen the file with open() again if you want to do additional operations
- *
- * @param string $outdir the target directory for extracting
- * @param int|string $strip either the number of path components or a fixed prefix to strip
- * @param string $exclude a regular expression of files to exclude
- * @param string $include a regular expression of files to include
- * @throws ArchiveIOException
- * @throws ArchiveCorruptedException
- * @return FileInfo[]
- */
- public function extract($outdir, $strip = '', $exclude = '', $include = '')
- {
- if ($this->closed || !$this->file) {
- throw new ArchiveIOException('Can not read from a closed archive');
- }
-
- $outdir = rtrim($outdir, '/');
- @mkdir($outdir, 0777, true);
- if (!is_dir($outdir)) {
- throw new ArchiveIOException("Could not create directory '$outdir'");
- }
-
- $extracted = array();
- while ($dat = $this->readbytes(512)) {
- // read the file header
- $header = $this->parseHeader($dat);
- if (!is_array($header)) {
- continue;
- }
- $fileinfo = $this->header2fileinfo($header);
-
- // apply strip rules
- $fileinfo->strip($strip);
-
- // skip unwanted files
- if (!strlen($fileinfo->getPath()) || !$fileinfo->matchExpression($include, $exclude)) {
- $this->skipbytes(ceil($header['size'] / 512) * 512);
- continue;
- }
-
- // create output directory
- $output = $outdir.'/'.$fileinfo->getPath();
- $directory = ($fileinfo->getIsdir()) ? $output : dirname($output);
- if (!file_exists($directory)) {
- mkdir($directory, 0777, true);
- }
-
- // extract data
- if (!$fileinfo->getIsdir()) {
- $fp = @fopen($output, "wb");
- if (!$fp) {
- throw new ArchiveIOException('Could not open file for writing: '.$output);
- }
-
- $size = floor($header['size'] / 512);
- for ($i = 0; $i < $size; $i++) {
- fwrite($fp, $this->readbytes(512), 512);
- }
- if (($header['size'] % 512) != 0) {
- fwrite($fp, $this->readbytes(512), $header['size'] % 512);
- }
-
- fclose($fp);
- @touch($output, $fileinfo->getMtime());
- @chmod($output, $fileinfo->getMode());
- } else {
- $this->skipbytes(ceil($header['size'] / 512) * 512); // the size is usually 0 for directories
- }
-
- if(is_callable($this->callback)) {
- call_user_func($this->callback, $fileinfo);
- }
- $extracted[] = $fileinfo;
- }
-
- $this->close();
- return $extracted;
- }
-
- /**
- * Create a new TAR file
- *
- * If $file is empty, the tar file will be created in memory
- *
- * @param string $file
- * @throws ArchiveIOException
- * @throws ArchiveIllegalCompressionException
- */
- public function create($file = '')
- {
- $this->file = $file;
- $this->memory = '';
- $this->fh = 0;
-
- if ($this->file) {
- // determine compression
- if ($this->comptype == Archive::COMPRESS_AUTO) {
- $this->setCompression($this->complevel, $this->filetype($file));
- }
-
- if ($this->comptype === Archive::COMPRESS_GZIP) {
- $this->fh = @gzopen($this->file, 'wb'.$this->complevel);
- } elseif ($this->comptype === Archive::COMPRESS_BZIP) {
- $this->fh = @bzopen($this->file, 'w');
- } else {
- $this->fh = @fopen($this->file, 'wb');
- }
-
- if (!$this->fh) {
- throw new ArchiveIOException('Could not open file for writing: '.$this->file);
- }
- }
- $this->writeaccess = true;
- $this->closed = false;
- }
-
- /**
- * Add a file to the current TAR archive using an existing file in the filesystem
- *
- * @param string $file path to the original file
- * @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data, empty to take from original
- * @throws ArchiveCorruptedException when the file changes while reading it, the archive will be corrupt and should be deleted
- * @throws ArchiveIOException there was trouble reading the given file, it was not added
- * @throws FileInfoException trouble reading file info, it was not added
- */
- public function addFile($file, $fileinfo = '')
- {
- if (is_string($fileinfo)) {
- $fileinfo = FileInfo::fromPath($file, $fileinfo);
- }
-
- if ($this->closed) {
- throw new ArchiveIOException('Archive has been closed, files can no longer be added');
- }
-
- // create file header
- $this->writeFileHeader($fileinfo);
-
- // write data, but only if we have data to write.
- // note: on Windows fopen() on a directory will fail, so we prevent
- // errors on Windows by testing if we have data to write.
- if (!$fileinfo->getIsdir() && $fileinfo->getSize() > 0) {
- $read = 0;
- $fp = @fopen($file, 'rb');
- if (!$fp) {
- throw new ArchiveIOException('Could not open file for reading: ' . $file);
- }
- while (!feof($fp)) {
- // for performance reasons read bigger chunks at once
- $data = fread($fp, self::READ_CHUNK_SIZE);
- if ($data === false) {
- break;
- }
- if ($data === '') {
- break;
- }
- $dataLen = strlen($data);
- $read += $dataLen;
- // how much of data read fully fills 512-byte blocks?
- $passLen = ($dataLen >> 9) << 9;
- if ($passLen === $dataLen) {
- // all - just write the data
- $this->writebytes($data);
- } else {
- // directly write what fills 512-byte blocks fully
- $this->writebytes(substr($data, 0, $passLen));
- // pad the reminder to 512 bytes
- $this->writebytes(pack("a512", substr($data, $passLen)));
- }
- }
- fclose($fp);
-
- if ($read != $fileinfo->getSize()) {
- $this->close();
- throw new ArchiveCorruptedException("The size of $file changed while reading, archive corrupted. read $read expected ".$fileinfo->getSize());
- }
- }
-
- if(is_callable($this->callback)) {
- call_user_func($this->callback, $fileinfo);
- }
- }
-
- /**
- * Add a file to the current TAR archive using the given $data as content
- *
- * @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data
- * @param string $data binary content of the file to add
- * @throws ArchiveIOException
- */
- public function addData($fileinfo, $data)
- {
- if (is_string($fileinfo)) {
- $fileinfo = new FileInfo($fileinfo);
- }
-
- if ($this->closed) {
- throw new ArchiveIOException('Archive has been closed, files can no longer be added');
- }
-
- $len = strlen($data);
- $fileinfo->setSize($len);
- $this->writeFileHeader($fileinfo);
-
- // write directly everything but the last block which needs padding
- $passLen = ($len >> 9) << 9;
- $this->writebytes(substr($data, 0, $passLen));
- if ($passLen < $len) {
- $this->writebytes(pack("a512", substr($data, $passLen, 512)));
- }
-
- if (is_callable($this->callback)) {
- call_user_func($this->callback, $fileinfo);
- }
- }
-
- /**
- * Add the closing footer to the archive if in write mode, close all file handles
- *
- * After a call to this function no more data can be added to the archive, for
- * read access no reading is allowed anymore
- *
- * "Physically, an archive consists of a series of file entries terminated by an end-of-archive entry, which
- * consists of two 512 blocks of zero bytes"
- *
- * @link http://www.gnu.org/software/tar/manual/html_chapter/tar_8.html#SEC134
- * @throws ArchiveIOException
- */
- public function close()
- {
- if ($this->closed) {
- return;
- } // we did this already
-
- // write footer
- if ($this->writeaccess) {
- $this->writebytes(pack("a512", ""));
- $this->writebytes(pack("a512", ""));
- }
-
- // close file handles
- if ($this->file) {
- if ($this->comptype === Archive::COMPRESS_GZIP) {
- gzclose($this->fh);
- } elseif ($this->comptype === Archive::COMPRESS_BZIP) {
- bzclose($this->fh);
- } else {
- fclose($this->fh);
- }
-
- $this->file = '';
- $this->fh = 0;
- }
-
- $this->writeaccess = false;
- $this->closed = true;
- }
-
- /**
- * Returns the created in-memory archive data
- *
- * This implicitly calls close() on the Archive
- * @throws ArchiveIOException
- */
- public function getArchive()
- {
- $this->close();
-
- if ($this->comptype === Archive::COMPRESS_AUTO) {
- $this->comptype = Archive::COMPRESS_NONE;
- }
-
- if ($this->comptype === Archive::COMPRESS_GZIP) {
- return gzencode($this->memory, $this->complevel);
- }
- if ($this->comptype === Archive::COMPRESS_BZIP) {
- return bzcompress($this->memory);
- }
- return $this->memory;
- }
-
- /**
- * Save the created in-memory archive data
- *
- * Note: It more memory effective to specify the filename in the create() function and
- * let the library work on the new file directly.
- *
- * @param string $file
- * @throws ArchiveIOException
- * @throws ArchiveIllegalCompressionException
- */
- public function save($file)
- {
- if ($this->comptype === Archive::COMPRESS_AUTO) {
- $this->setCompression($this->complevel, $this->filetype($file));
- }
-
- if (!@file_put_contents($file, $this->getArchive())) {
- throw new ArchiveIOException('Could not write to file: '.$file);
- }
- }
-
- /**
- * Read from the open file pointer
- *
- * @param int $length bytes to read
- * @return string
- */
- protected function readbytes($length)
- {
- if ($this->comptype === Archive::COMPRESS_GZIP) {
- $ret = @gzread($this->fh, $length);
- } elseif ($this->comptype === Archive::COMPRESS_BZIP) {
- $ret = @bzread($this->fh, $length);
- } else {
- $ret = @fread($this->fh, $length);
- }
- $this->position += strlen($ret);
- return $ret;
- }
-
- /**
- * Write to the open filepointer or memory
- *
- * @param string $data
- * @throws ArchiveIOException
- * @return int number of bytes written
- */
- protected function writebytes($data)
- {
- if (!$this->file) {
- $this->memory .= $data;
- $written = strlen($data);
- } elseif ($this->comptype === Archive::COMPRESS_GZIP) {
- $written = @gzwrite($this->fh, $data);
- } elseif ($this->comptype === Archive::COMPRESS_BZIP) {
- $written = @bzwrite($this->fh, $data);
- } else {
- $written = @fwrite($this->fh, $data);
- }
- if ($written === false) {
- throw new ArchiveIOException('Failed to write to archive stream');
- }
- return $written;
- }
-
- /**
- * Skip forward in the open file pointer
- *
- * This is basically a wrapper around seek() (and a workaround for bzip2)
- *
- * @param int $bytes seek to this position
- */
- protected function skipbytes($bytes)
- {
- if ($this->comptype === Archive::COMPRESS_GZIP) {
- @gzseek($this->fh, $bytes, SEEK_CUR);
- } elseif ($this->comptype === Archive::COMPRESS_BZIP) {
- // there is no seek in bzip2, we simply read on
- // bzread allows to read a max of 8kb at once
- while($bytes) {
- $toread = min(8192, $bytes);
- @bzread($this->fh, $toread);
- $bytes -= $toread;
- }
- } else {
- @fseek($this->fh, $bytes, SEEK_CUR);
- }
- $this->position += $bytes;
- }
-
- /**
- * Write the given file meta data as header
- *
- * @param FileInfo $fileinfo
- * @throws ArchiveIOException
- */
- protected function writeFileHeader(FileInfo $fileinfo)
- {
- $this->writeRawFileHeader(
- $fileinfo->getPath(),
- $fileinfo->getUid(),
- $fileinfo->getGid(),
- $fileinfo->getMode(),
- $fileinfo->getSize(),
- $fileinfo->getMtime(),
- $fileinfo->getIsdir() ? '5' : '0'
- );
- }
-
- /**
- * Write a file header to the stream
- *
- * @param string $name
- * @param int $uid
- * @param int $gid
- * @param int $perm
- * @param int $size
- * @param int $mtime
- * @param string $typeflag Set to '5' for directories
- * @throws ArchiveIOException
- */
- protected function writeRawFileHeader($name, $uid, $gid, $perm, $size, $mtime, $typeflag = '')
- {
- // handle filename length restrictions
- $prefix = '';
- $namelen = strlen($name);
- if ($namelen > 100) {
- $file = basename($name);
- $dir = dirname($name);
- if (strlen($file) > 100 || strlen($dir) > 155) {
- // we're still too large, let's use GNU longlink
- $this->writeRawFileHeader('././@LongLink', 0, 0, 0, $namelen, 0, 'L');
- for ($s = 0; $s < $namelen; $s += 512) {
- $this->writebytes(pack("a512", substr($name, $s, 512)));
- }
- $name = substr($name, 0, 100); // cut off name
- } else {
- // we're fine when splitting, use POSIX ustar
- $prefix = $dir;
- $name = $file;
- }
- }
-
- // values are needed in octal
- $uid = sprintf("%6s ", decoct($uid));
- $gid = sprintf("%6s ", decoct($gid));
- $perm = sprintf("%6s ", decoct($perm));
- $size = self::numberEncode($size, 12);
- $mtime = self::numberEncode($mtime, 12);
-
- $data_first = pack("a100a8a8a8a12A12", $name, $perm, $uid, $gid, $size, $mtime);
- $data_last = pack("a1a100a6a2a32a32a8a8a155a12", $typeflag, '', 'ustar', '', '', '', '', '', $prefix, "");
-
- for ($i = 0, $chks = 0; $i < 148; $i++) {
- $chks += ord($data_first[$i]);
- }
-
- for ($i = 156, $chks += 256, $j = 0; $i < 512; $i++, $j++) {
- $chks += ord($data_last[$j]);
- }
-
- $this->writebytes($data_first);
-
- $chks = pack("a8", sprintf("%6s ", decoct($chks)));
- $this->writebytes($chks.$data_last);
- }
-
- /**
- * Decode the given tar file header
- *
- * @param string $block a 512 byte block containing the header data
- * @return array|false returns false when this was a null block
- * @throws ArchiveCorruptedException
- */
- protected function parseHeader($block)
- {
- if (!$block || strlen($block) != 512) {
- throw new ArchiveCorruptedException('Unexpected length of header');
- }
-
- // null byte blocks are ignored
- if(trim($block) === '') return false;
-
- for ($i = 0, $chks = 0; $i < 148; $i++) {
- $chks += ord($block[$i]);
- }
-
- for ($i = 156, $chks += 256; $i < 512; $i++) {
- $chks += ord($block[$i]);
- }
-
- $header = @unpack(
- "a100filename/a8perm/a8uid/a8gid/a12size/a12mtime/a8checksum/a1typeflag/a100link/a6magic/a2version/a32uname/a32gname/a8devmajor/a8devminor/a155prefix",
- $block
- );
- if (!$header) {
- throw new ArchiveCorruptedException('Failed to parse header');
- }
-
- $return['checksum'] = OctDec(trim($header['checksum']));
- if ($return['checksum'] != $chks) {
- throw new ArchiveCorruptedException('Header does not match its checksum');
- }
-
- $return['filename'] = trim($header['filename']);
- $return['perm'] = OctDec(trim($header['perm']));
- $return['uid'] = OctDec(trim($header['uid']));
- $return['gid'] = OctDec(trim($header['gid']));
- $return['size'] = self::numberDecode($header['size']);
- $return['mtime'] = self::numberDecode($header['mtime']);
- $return['typeflag'] = $header['typeflag'];
- $return['link'] = trim($header['link']);
- $return['uname'] = trim($header['uname']);
- $return['gname'] = trim($header['gname']);
-
- // Handle ustar Posix compliant path prefixes
- if (trim($header['prefix'])) {
- $return['filename'] = trim($header['prefix']).'/'.$return['filename'];
- }
-
- // Handle Long-Link entries from GNU Tar
- if ($return['typeflag'] == 'L') {
- // following data block(s) is the filename
- $filename = trim($this->readbytes(ceil($return['size'] / 512) * 512));
- // next block is the real header
- $block = $this->readbytes(512);
- $return = $this->parseHeader($block);
- // overwrite the filename
- $return['filename'] = $filename;
- }
-
- return $return;
- }
-
- /**
- * Creates a FileInfo object from the given parsed header
- *
- * @param $header
- * @return FileInfo
- */
- protected function header2fileinfo($header)
- {
- $fileinfo = new FileInfo();
- $fileinfo->setPath($header['filename']);
- $fileinfo->setMode($header['perm']);
- $fileinfo->setUid($header['uid']);
- $fileinfo->setGid($header['gid']);
- $fileinfo->setSize($header['size']);
- $fileinfo->setMtime($header['mtime']);
- $fileinfo->setOwner($header['uname']);
- $fileinfo->setGroup($header['gname']);
- $fileinfo->setIsdir((bool) $header['typeflag']);
-
- return $fileinfo;
- }
-
- /**
- * Checks if the given compression type is available and throws an exception if not
- *
- * @param $comptype
- * @throws ArchiveIllegalCompressionException
- */
- protected function compressioncheck($comptype)
- {
- if ($comptype === Archive::COMPRESS_GZIP && !function_exists('gzopen')) {
- throw new ArchiveIllegalCompressionException('No gzip support available');
- }
-
- if ($comptype === Archive::COMPRESS_BZIP && !function_exists('bzopen')) {
- throw new ArchiveIllegalCompressionException('No bzip2 support available');
- }
- }
-
- /**
- * Guesses the wanted compression from the given file
- *
- * Uses magic bytes for existing files, the file extension otherwise
- *
- * You don't need to call this yourself. It's used when you pass Archive::COMPRESS_AUTO somewhere
- *
- * @param string $file
- * @return int
- */
- public function filetype($file)
- {
- // for existing files, try to read the magic bytes
- if(file_exists($file) && is_readable($file) && filesize($file) > 5) {
- $fh = @fopen($file, 'rb');
- if(!$fh) return false;
- $magic = fread($fh, 5);
- fclose($fh);
-
- if(strpos($magic, "\x42\x5a") === 0) return Archive::COMPRESS_BZIP;
- if(strpos($magic, "\x1f\x8b") === 0) return Archive::COMPRESS_GZIP;
- }
-
- // otherwise rely on file name
- $file = strtolower($file);
- if (substr($file, -3) == '.gz' || substr($file, -4) == '.tgz') {
- return Archive::COMPRESS_GZIP;
- } elseif (substr($file, -4) == '.bz2' || substr($file, -4) == '.tbz') {
- return Archive::COMPRESS_BZIP;
- }
-
- return Archive::COMPRESS_NONE;
- }
-
- /**
- * Decodes numeric values according to the
- * https://www.gnu.org/software/tar/manual/html_node/Extensions.html#Extensions
- * (basically with support for big numbers)
- *
- * @param string $field
- * $return int
- */
- static public function numberDecode($field)
- {
- $firstByte = ord(substr($field, 0, 1));
- if ($firstByte === 255) {
- $value = -1 << (8 * strlen($field));
- $shift = 0;
- for ($i = strlen($field) - 1; $i >= 0; $i--) {
- $value += ord(substr($field, $i, 1)) << $shift;
- $shift += 8;
- }
- } elseif ($firstByte === 128) {
- $value = 0;
- $shift = 0;
- for ($i = strlen($field) - 1; $i > 0; $i--) {
- $value += ord(substr($field, $i, 1)) << $shift;
- $shift += 8;
- }
- } else {
- $value = octdec(trim($field));
- }
- return $value;
- }
-
- /**
- * Encodes numeric values according to the
- * https://www.gnu.org/software/tar/manual/html_node/Extensions.html#Extensions
- * (basically with support for big numbers)
- *
- * @param int $value
- * @param int $length field length
- * @return string
- */
- static public function numberEncode($value, $length)
- {
- // old implementations leave last byte empty
- // octal encoding encodes three bits per byte
- $maxValue = 1 << (($length - 1) * 3);
- if ($value < 0) {
- // PHP already stores integers as 2's complement
- $value = pack(PHP_INT_SIZE === 8 ? 'J' : 'N', (int) $value);
- $encoded = str_repeat(chr(255), max(1, $length - PHP_INT_SIZE));
- $encoded .= substr($value, max(0, PHP_INT_SIZE - $length + 1));
- } elseif ($value >= $maxValue) {
- $value = pack(PHP_INT_SIZE === 8 ? 'J' : 'N', (int) $value);
- $encoded = chr(128) . str_repeat(chr(0), max(0, $length - PHP_INT_SIZE - 1));
- $encoded .= substr($value, max(0, PHP_INT_SIZE - $length + 1));
- } else {
- $encoded = sprintf("%" . ($length - 1) . "s ", decoct($value));
- }
- return $encoded;
- }
-}
-
diff --git a/src/Zip.php b/src/Zip.php
deleted file mode 100644
index 7b8030c..0000000
--- a/src/Zip.php
+++ /dev/null
@@ -1,1026 +0,0 @@
-
- * @package splitbrain\PHPArchive
- * @license MIT
- */
-class Zip extends Archive
-{
- const LOCAL_FILE_HEADER_CRC_OFFSET = 14;
-
- protected $file = '';
- protected $fh;
- protected $memory = '';
- protected $closed = true;
- protected $writeaccess = false;
- protected $ctrl_dir;
- protected $complevel = 9;
-
- /**
- * Set the compression level.
- *
- * Compression Type is ignored for ZIP
- *
- * You can call this function before adding each file to set differen compression levels
- * for each file.
- *
- * @param int $level Compression level (0 to 9)
- * @param int $type Type of compression to use ignored for ZIP
- * @throws ArchiveIllegalCompressionException
- */
- public function setCompression($level = 9, $type = Archive::COMPRESS_AUTO)
- {
- if ($level < -1 || $level > 9) {
- throw new ArchiveIllegalCompressionException('Compression level should be between -1 and 9');
- }
- $this->complevel = $level;
- }
-
- /**
- * Open an existing ZIP file for reading
- *
- * @param string $file
- * @throws ArchiveIOException
- */
- public function open($file)
- {
- $this->file = $file;
- $this->fh = @fopen($this->file, 'rb');
- if (!$this->fh) {
- throw new ArchiveIOException('Could not open file for reading: '.$this->file);
- }
- $this->closed = false;
- }
-
- /**
- * Read the contents of a ZIP archive
- *
- * This function lists the files stored in the archive, and returns an indexed array of FileInfo objects
- *
- * The archive is closed afer reading the contents, for API compatibility with TAR files
- * Reopen the file with open() again if you want to do additional operations
- *
- * @throws ArchiveIOException
- * @return FileInfo[]
- */
- public function contents()
- {
- $result = array();
-
- foreach ($this->yieldContents() as $fileinfo) {
- $result[] = $fileinfo;
- }
-
- return $result;
- }
-
- /**
- * Read the contents of a ZIP archive and return each entry using yield
- * for memory efficiency.
- *
- * @see contents()
- * @throws ArchiveIOException
- * @return FileInfo[]
- */
- public function yieldContents()
- {
- if ($this->closed || !$this->file) {
- throw new ArchiveIOException('Can not read from a closed archive');
- }
-
- $centd = $this->readCentralDir();
-
- @rewind($this->fh);
- @fseek($this->fh, $centd['offset']);
-
- for ($i = 0; $i < $centd['entries']; $i++) {
- yield $this->header2fileinfo($this->readCentralFileHeader());
- }
-
- $this->close();
- }
-
- /**
- * Extract an existing ZIP archive
- *
- * The $strip parameter allows you to strip a certain number of path components from the filenames
- * found in the tar file, similar to the --strip-components feature of GNU tar. This is triggered when
- * an integer is passed as $strip.
- * Alternatively a fixed string prefix may be passed in $strip. If the filename matches this prefix,
- * the prefix will be stripped. It is recommended to give prefixes with a trailing slash.
- *
- * By default this will extract all files found in the archive. You can restrict the output using the $include
- * and $exclude parameter. Both expect a full regular expression (including delimiters and modifiers). If
- * $include is set only files that match this expression will be extracted. Files that match the $exclude
- * expression will never be extracted. Both parameters can be used in combination. Expressions are matched against
- * stripped filenames as described above.
- *
- * @param string $outdir the target directory for extracting
- * @param int|string $strip either the number of path components or a fixed prefix to strip
- * @param string $exclude a regular expression of files to exclude
- * @param string $include a regular expression of files to include
- * @throws ArchiveIOException
- * @return FileInfo[]
- */
- public function extract($outdir, $strip = '', $exclude = '', $include = '')
- {
- if ($this->closed || !$this->file) {
- throw new ArchiveIOException('Can not read from a closed archive');
- }
-
- $outdir = rtrim($outdir, '/');
- @mkdir($outdir, 0777, true);
-
- $extracted = array();
-
- $cdir = $this->readCentralDir();
- $pos_entry = $cdir['offset']; // begin of the central file directory
-
- for ($i = 0; $i < $cdir['entries']; $i++) {
- // read file header
- @fseek($this->fh, $pos_entry);
- $header = $this->readCentralFileHeader();
- $header['index'] = $i;
- $pos_entry = ftell($this->fh); // position of the next file in central file directory
- fseek($this->fh, $header['offset']); // seek to beginning of file header
- $header = $this->readFileHeader($header);
- $fileinfo = $this->header2fileinfo($header);
-
- // apply strip rules
- $fileinfo->strip($strip);
-
- // skip unwanted files
- if (!strlen($fileinfo->getPath()) || !$fileinfo->matchExpression($include, $exclude)) {
- continue;
- }
-
- $extracted[] = $fileinfo;
-
- // create output directory
- $output = $outdir.'/'.$fileinfo->getPath();
- $directory = ($header['folder']) ? $output : dirname($output);
- @mkdir($directory, 0777, true);
-
- // nothing more to do for directories
- if ($fileinfo->getIsdir()) {
- if(is_callable($this->callback)) {
- call_user_func($this->callback, $fileinfo);
- }
- continue;
- }
-
- // compressed files are written to temporary .gz file first
- if ($header['compression'] == 0) {
- $extractto = $output;
- } else {
- $extractto = $output.'.gz';
- }
-
- // open file for writing
- $fp = @fopen($extractto, "wb");
- if (!$fp) {
- throw new ArchiveIOException('Could not open file for writing: '.$extractto);
- }
-
- // prepend compression header
- if ($header['compression'] != 0) {
- $binary_data = pack(
- 'va1a1Va1a1',
- 0x8b1f,
- chr($header['compression']),
- chr(0x00),
- time(),
- chr(0x00),
- chr(3)
- );
- fwrite($fp, $binary_data, 10);
- }
-
- // read the file and store it on disk
- $size = $header['compressed_size'];
- while ($size != 0) {
- $read_size = ($size < 2048 ? $size : 2048);
- $buffer = fread($this->fh, $read_size);
- $binary_data = pack('a'.$read_size, $buffer);
- fwrite($fp, $binary_data, $read_size);
- $size -= $read_size;
- }
-
- // finalize compressed file
- if ($header['compression'] != 0) {
- $binary_data = pack('VV', $header['crc'], $header['size']);
- fwrite($fp, $binary_data, 8);
- }
-
- // close file
- fclose($fp);
-
- // unpack compressed file
- if ($header['compression'] != 0) {
- $gzp = @gzopen($extractto, 'rb');
- if (!$gzp) {
- @unlink($extractto);
- throw new ArchiveIOException('Failed file extracting. gzip support missing?');
- }
- $fp = @fopen($output, 'wb');
- if (!$fp) {
- throw new ArchiveIOException('Could not open file for writing: '.$extractto);
- }
-
- $size = $header['size'];
- while ($size != 0) {
- $read_size = ($size < 2048 ? $size : 2048);
- $buffer = gzread($gzp, $read_size);
- $binary_data = pack('a'.$read_size, $buffer);
- @fwrite($fp, $binary_data, $read_size);
- $size -= $read_size;
- }
- fclose($fp);
- gzclose($gzp);
- unlink($extractto); // remove temporary gz file
- }
-
- @touch($output, $fileinfo->getMtime());
- //FIXME what about permissions?
- if(is_callable($this->callback)) {
- call_user_func($this->callback, $fileinfo);
- }
- }
-
- $this->close();
- return $extracted;
- }
-
- /**
- * Create a new ZIP file
- *
- * If $file is empty, the zip file will be created in memory
- *
- * @param string $file
- * @throws ArchiveIOException
- */
- public function create($file = '')
- {
- $this->file = $file;
- $this->memory = '';
- $this->fh = 0;
-
- if ($this->file) {
- $this->fh = @fopen($this->file, 'wb');
-
- if (!$this->fh) {
- throw new ArchiveIOException('Could not open file for writing: '.$this->file);
- }
- }
- $this->writeaccess = true;
- $this->closed = false;
- $this->ctrl_dir = array();
- }
-
- /**
- * Add a file to the current ZIP archive using an existing file in the filesystem
- *
- * @param string $file path to the original file
- * @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data, empty to take from original
- * @throws ArchiveIOException
- */
-
- /**
- * Add a file to the current archive using an existing file in the filesystem
- *
- * @param string $file path to the original file
- * @param string|FileInfo $fileinfo either the name to use in archive (string) or a FileInfo oject with all meta data, empty to take from original
- * @throws ArchiveIOException
- * @throws FileInfoException
- */
- public function addFile($file, $fileinfo = '')
- {
- if (is_string($fileinfo)) {
- $fileinfo = FileInfo::fromPath($file, $fileinfo);
- }
-
- if ($this->closed) {
- throw new ArchiveIOException('Archive has been closed, files can no longer be added');
- }
-
- $fp = @fopen($file, 'rb');
- if ($fp === false) {
- throw new ArchiveIOException('Could not open file for reading: '.$file);
- }
-
- $offset = $this->dataOffset();
- $name = $fileinfo->getPath();
- $time = $fileinfo->getMtime();
-
- // write local file header (temporary CRC and size)
- $this->writebytes($this->makeLocalFileHeader(
- $time,
- 0,
- 0,
- 0,
- $name,
- (bool) $this->complevel
- ));
-
- // we store no encryption header
-
- // prepare info, compress and write data to archive
- $deflate_context = deflate_init(ZLIB_ENCODING_DEFLATE, ['level' => $this->complevel]);
- $crc_context = hash_init('crc32b');
- $size = $csize = 0;
-
- while (!feof($fp)) {
- $block = fread($fp, 512);
-
- if ($this->complevel) {
- $is_first_block = $size === 0;
- $is_last_block = feof($fp);
-
- if ($is_last_block) {
- $c_block = deflate_add($deflate_context, $block, ZLIB_FINISH);
- // get rid of the compression footer
- $c_block = substr($c_block, 0, -4);
- } else {
- $c_block = deflate_add($deflate_context, $block, ZLIB_NO_FLUSH);
- }
-
- // get rid of the compression header
- if ($is_first_block) {
- $c_block = substr($c_block, 2);
- }
-
- $csize += strlen($c_block);
- $this->writebytes($c_block);
- } else {
- $this->writebytes($block);
- }
-
- $size += strlen($block);
- hash_update($crc_context, $block);
- }
- fclose($fp);
-
- // update the local file header with the computed CRC and size
- $crc = hexdec(hash_final($crc_context));
- $csize = $this->complevel ? $csize : $size;
- $this->writebytesAt($this->makeCrcAndSize(
- $crc,
- $size,
- $csize
- ), $offset + self::LOCAL_FILE_HEADER_CRC_OFFSET);
-
- // we store no data descriptor
-
- // add info to central file directory
- $this->ctrl_dir[] = $this->makeCentralFileRecord(
- $offset,
- $time,
- $crc,
- $size,
- $csize,
- $name,
- (bool) $this->complevel
- );
-
- if(is_callable($this->callback)) {
- call_user_func($this->callback, $fileinfo);
- }
- }
-
- /**
- * Add a file to the current Zip archive using the given $data as content
- *
- * @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data
- * @param string $data binary content of the file to add
- * @throws ArchiveIOException
- */
- public function addData($fileinfo, $data)
- {
- if (is_string($fileinfo)) {
- $fileinfo = new FileInfo($fileinfo);
- }
-
- if ($this->closed) {
- throw new ArchiveIOException('Archive has been closed, files can no longer be added');
- }
-
- // prepare info and compress data
- $size = strlen($data);
- $crc = crc32($data);
- if ($this->complevel) {
- $data = gzcompress($data, $this->complevel);
- $data = substr($data, 2, -4); // strip compression headers
- }
- $csize = strlen($data);
- $offset = $this->dataOffset();
- $name = $fileinfo->getPath();
- $time = $fileinfo->getMtime();
-
- // write local file header
- $this->writebytes($this->makeLocalFileHeader(
- $time,
- $crc,
- $size,
- $csize,
- $name,
- (bool) $this->complevel
- ));
-
- // we store no encryption header
-
- // write data
- $this->writebytes($data);
-
- // we store no data descriptor
-
- // add info to central file directory
- $this->ctrl_dir[] = $this->makeCentralFileRecord(
- $offset,
- $time,
- $crc,
- $size,
- $csize,
- $name,
- (bool) $this->complevel
- );
-
- if(is_callable($this->callback)) {
- call_user_func($this->callback, $fileinfo);
- }
- }
-
- /**
- * Add the closing footer to the archive if in write mode, close all file handles
- *
- * After a call to this function no more data can be added to the archive, for
- * read access no reading is allowed anymore
- * @throws ArchiveIOException
- */
- public function close()
- {
- if ($this->closed) {
- return;
- } // we did this already
-
- if ($this->writeaccess) {
- // write central directory
- $offset = $this->dataOffset();
- $ctrldir = join('', $this->ctrl_dir);
- $this->writebytes($ctrldir);
-
- // write end of central directory record
- $this->writebytes("\x50\x4b\x05\x06"); // end of central dir signature
- $this->writebytes(pack('v', 0)); // number of this disk
- $this->writebytes(pack('v', 0)); // number of the disk with the start of the central directory
- $this->writebytes(pack('v',
- count($this->ctrl_dir))); // total number of entries in the central directory on this disk
- $this->writebytes(pack('v', count($this->ctrl_dir))); // total number of entries in the central directory
- $this->writebytes(pack('V', strlen($ctrldir))); // size of the central directory
- $this->writebytes(pack('V',
- $offset)); // offset of start of central directory with respect to the starting disk number
- $this->writebytes(pack('v', 0)); // .ZIP file comment length
-
- $this->ctrl_dir = array();
- }
-
- // close file handles
- if ($this->file) {
- fclose($this->fh);
- $this->file = '';
- $this->fh = 0;
- }
-
- $this->writeaccess = false;
- $this->closed = true;
- }
-
- /**
- * Returns the created in-memory archive data
- *
- * This implicitly calls close() on the Archive
- * @throws ArchiveIOException
- */
- public function getArchive()
- {
- $this->close();
-
- return $this->memory;
- }
-
- /**
- * Save the created in-memory archive data
- *
- * Note: It's more memory effective to specify the filename in the create() function and
- * let the library work on the new file directly.
- *
- * @param $file
- * @throws ArchiveIOException
- */
- public function save($file)
- {
- if (!@file_put_contents($file, $this->getArchive())) {
- throw new ArchiveIOException('Could not write to file: '.$file);
- }
- }
-
- /**
- * Read the central directory
- *
- * This key-value list contains general information about the ZIP file
- *
- * @return array
- */
- protected function readCentralDir()
- {
- $size = filesize($this->file);
- if ($size < 277) {
- $maximum_size = $size;
- } else {
- $maximum_size = 277;
- }
-
- @fseek($this->fh, $size - $maximum_size);
- $pos = ftell($this->fh);
- $bytes = 0x00000000;
-
- while ($pos < $size) {
- $byte = @fread($this->fh, 1);
- $bytes = (($bytes << 8) & 0xFFFFFFFF) | ord($byte);
- if ($bytes == 0x504b0506) {
- break;
- }
- $pos++;
- }
-
- $data = unpack(
- 'vdisk/vdisk_start/vdisk_entries/ventries/Vsize/Voffset/vcomment_size',
- fread($this->fh, 18)
- );
-
- if ($data['comment_size'] != 0) {
- $centd['comment'] = fread($this->fh, $data['comment_size']);
- } else {
- $centd['comment'] = '';
- }
- $centd['entries'] = $data['entries'];
- $centd['disk_entries'] = $data['disk_entries'];
- $centd['offset'] = $data['offset'];
- $centd['disk_start'] = $data['disk_start'];
- $centd['size'] = $data['size'];
- $centd['disk'] = $data['disk'];
- return $centd;
- }
-
- /**
- * Read the next central file header
- *
- * Assumes the current file pointer is pointing at the right position
- *
- * @return array
- */
- protected function readCentralFileHeader()
- {
- $binary_data = fread($this->fh, 46);
- $header = unpack(
- 'vchkid/vid/vversion/vversion_extracted/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len/vcomment_len/vdisk/vinternal/Vexternal/Voffset',
- $binary_data
- );
-
- if ($header['filename_len'] != 0) {
- $header['filename'] = fread($this->fh, $header['filename_len']);
- } else {
- $header['filename'] = '';
- }
-
- if ($header['extra_len'] != 0) {
- $header['extra'] = fread($this->fh, $header['extra_len']);
- $header['extradata'] = $this->parseExtra($header['extra']);
- } else {
- $header['extra'] = '';
- $header['extradata'] = array();
- }
-
- if ($header['comment_len'] != 0) {
- $header['comment'] = fread($this->fh, $header['comment_len']);
- } else {
- $header['comment'] = '';
- }
-
- $header['mtime'] = $this->makeUnixTime($header['mdate'], $header['mtime']);
- $header['stored_filename'] = $header['filename'];
- $header['status'] = 'ok';
- if (substr($header['filename'], -1) == '/') {
- $header['external'] = 0x41FF0010;
- }
- $header['folder'] = ($header['external'] == 0x41FF0010 || $header['external'] == 16) ? 1 : 0;
-
- return $header;
- }
-
- /**
- * Reads the local file header
- *
- * This header precedes each individual file inside the zip file. Assumes the current file pointer is pointing at
- * the right position already. Enhances the given central header with the data found at the local header.
- *
- * @param array $header the central file header read previously (see above)
- * @return array
- */
- protected function readFileHeader($header)
- {
- $binary_data = fread($this->fh, 30);
- $data = unpack(
- 'vchk/vid/vversion/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len',
- $binary_data
- );
-
- $header['filename'] = fread($this->fh, $data['filename_len']);
- if ($data['extra_len'] != 0) {
- $header['extra'] = fread($this->fh, $data['extra_len']);
- $header['extradata'] = array_merge($header['extradata'], $this->parseExtra($header['extra']));
- } else {
- $header['extra'] = '';
- $header['extradata'] = array();
- }
-
- $header['compression'] = $data['compression'];
- foreach (array(
- 'size',
- 'compressed_size',
- 'crc'
- ) as $hd) { // On ODT files, these headers are 0. Keep the previous value.
- if ($data[$hd] != 0) {
- $header[$hd] = $data[$hd];
- }
- }
- $header['flag'] = $data['flag'];
- $header['mtime'] = $this->makeUnixTime($data['mdate'], $data['mtime']);
-
- $header['stored_filename'] = $header['filename'];
- $header['status'] = "ok";
- $header['folder'] = ($header['external'] == 0x41FF0010 || $header['external'] == 16) ? 1 : 0;
- return $header;
- }
-
- /**
- * Parse the extra headers into fields
- *
- * @param string $header
- * @return array
- */
- protected function parseExtra($header)
- {
- $extra = array();
- // parse all extra fields as raw values
- while (strlen($header) !== 0) {
- $set = unpack('vid/vlen', $header);
- $header = substr($header, 4);
- $value = substr($header, 0, $set['len']);
- $header = substr($header, $set['len']);
- $extra[$set['id']] = $value;
- }
-
- // handle known ones
- if(isset($extra[0x6375])) {
- $extra['utf8comment'] = substr($extra[0x7075], 5); // strip version and crc
- }
- if(isset($extra[0x7075])) {
- $extra['utf8path'] = substr($extra[0x7075], 5); // strip version and crc
- }
-
- return $extra;
- }
-
- /**
- * Create fileinfo object from header data
- *
- * @param $header
- * @return FileInfo
- */
- protected function header2fileinfo($header)
- {
- $fileinfo = new FileInfo();
- $fileinfo->setSize($header['size']);
- $fileinfo->setCompressedSize($header['compressed_size']);
- $fileinfo->setMtime($header['mtime']);
- $fileinfo->setComment($header['comment']);
- $fileinfo->setIsdir($header['external'] == 0x41FF0010 || $header['external'] == 16);
-
- if(isset($header['extradata']['utf8path'])) {
- $fileinfo->setPath($header['extradata']['utf8path']);
- } else {
- $fileinfo->setPath($this->cpToUtf8($header['filename']));
- }
-
- if(isset($header['extradata']['utf8comment'])) {
- $fileinfo->setComment($header['extradata']['utf8comment']);
- } else {
- $fileinfo->setComment($this->cpToUtf8($header['comment']));
- }
-
- return $fileinfo;
- }
-
- /**
- * Convert the given CP437 encoded string to UTF-8
- *
- * Tries iconv with the correct encoding first, falls back to mbstring with CP850 which is
- * similar enough. CP437 seems not to be available in mbstring. Lastly falls back to keeping the
- * string as is, which is still better than nothing.
- *
- * On some systems iconv is available, but the codepage is not. We also check for that.
- *
- * @param $string
- * @return string
- */
- protected function cpToUtf8($string)
- {
- if (function_exists('iconv') && @iconv_strlen('', 'CP437') !== false) {
- return iconv('CP437', 'UTF-8', $string);
- } elseif (function_exists('mb_convert_encoding')) {
- return mb_convert_encoding($string, 'UTF-8', 'CP850');
- } else {
- return $string;
- }
- }
-
- /**
- * Convert the given UTF-8 encoded string to CP437
- *
- * Same caveats as for cpToUtf8() apply
- *
- * @param $string
- * @return string
- */
- protected function utf8ToCp($string)
- {
- // try iconv first
- if (function_exists('iconv')) {
- $conv = @iconv('UTF-8', 'CP437//IGNORE', $string);
- if($conv) return $conv; // it worked
- }
-
- // still here? iconv failed to convert the string. Try another method
- // see http://php.net/manual/en/function.iconv.php#108643
-
- if (function_exists('mb_convert_encoding')) {
- return mb_convert_encoding($string, 'CP850', 'UTF-8');
- } else {
- return $string;
- }
- }
-
-
- /**
- * Write to the open filepointer or memory
- *
- * @param string $data
- * @throws ArchiveIOException
- * @return int number of bytes written
- */
- protected function writebytes($data)
- {
- if (!$this->file) {
- $this->memory .= $data;
- $written = strlen($data);
- } else {
- $written = @fwrite($this->fh, $data);
- }
- if ($written === false) {
- throw new ArchiveIOException('Failed to write to archive stream');
- }
- return $written;
- }
-
- /**
- * Write to the open filepointer or memory at the specified offset
- *
- * @param string $data
- * @param int $offset
- * @throws ArchiveIOException
- * @return int number of bytes written
- */
- protected function writebytesAt($data, $offset) {
- if (!$this->file) {
- $this->memory .= substr_replace($this->memory, $data, $offset);
- $written = strlen($data);
- } else {
- @fseek($this->fh, $offset);
- $written = @fwrite($this->fh, $data);
- @fseek($this->fh, 0, SEEK_END);
- }
- if ($written === false) {
- throw new ArchiveIOException('Failed to write to archive stream');
- }
- return $written;
- }
-
- /**
- * Current data pointer position
- *
- * @fixme might need a -1
- * @return int
- */
- protected function dataOffset()
- {
- if ($this->file) {
- return ftell($this->fh);
- } else {
- return strlen($this->memory);
- }
- }
-
- /**
- * Create a DOS timestamp from a UNIX timestamp
- *
- * DOS timestamps start at 1980-01-01, earlier UNIX stamps will be set to this date
- *
- * @param $time
- * @return int
- */
- protected function makeDosTime($time)
- {
- $timearray = getdate($time);
- if ($timearray['year'] < 1980) {
- $timearray['year'] = 1980;
- $timearray['mon'] = 1;
- $timearray['mday'] = 1;
- $timearray['hours'] = 0;
- $timearray['minutes'] = 0;
- $timearray['seconds'] = 0;
- }
- return (($timearray['year'] - 1980) << 25) |
- ($timearray['mon'] << 21) |
- ($timearray['mday'] << 16) |
- ($timearray['hours'] << 11) |
- ($timearray['minutes'] << 5) |
- ($timearray['seconds'] >> 1);
- }
-
- /**
- * Create a UNIX timestamp from a DOS timestamp
- *
- * @param $mdate
- * @param $mtime
- * @return int
- */
- protected function makeUnixTime($mdate = null, $mtime = null)
- {
- if ($mdate && $mtime) {
- $year = (($mdate & 0xFE00) >> 9) + 1980;
- $month = ($mdate & 0x01E0) >> 5;
- $day = $mdate & 0x001F;
-
- $hour = ($mtime & 0xF800) >> 11;
- $minute = ($mtime & 0x07E0) >> 5;
- $seconde = ($mtime & 0x001F) << 1;
-
- $mtime = mktime($hour, $minute, $seconde, $month, $day, $year);
- } else {
- $mtime = time();
- }
-
- return $mtime;
- }
-
- /**
- * Returns a local file header for the given data
- *
- * @param int $offset location of the local header
- * @param int $ts unix timestamp
- * @param int $crc CRC32 checksum of the uncompressed data
- * @param int $len length of the uncompressed data
- * @param int $clen length of the compressed data
- * @param string $name file name
- * @param boolean|null $comp if compression is used, if null it's determined from $len != $clen
- * @return string
- */
- protected function makeCentralFileRecord($offset, $ts, $crc, $len, $clen, $name, $comp = null)
- {
- if(is_null($comp)) $comp = $len != $clen;
- $comp = $comp ? 8 : 0;
- $dtime = dechex($this->makeDosTime($ts));
-
- list($name, $extra) = $this->encodeFilename($name);
-
- $header = "\x50\x4b\x01\x02"; // central file header signature
- $header .= pack('v', 14); // version made by - VFAT
- $header .= pack('v', 20); // version needed to extract - 2.0
- $header .= pack('v', 0); // general purpose flag - no flags set
- $header .= pack('v', $comp); // compression method - deflate|none
- $header .= pack(
- 'H*',
- $dtime[6] . $dtime[7] .
- $dtime[4] . $dtime[5] .
- $dtime[2] . $dtime[3] .
- $dtime[0] . $dtime[1]
- ); // last mod file time and date
- $header .= pack('V', $crc); // crc-32
- $header .= pack('V', $clen); // compressed size
- $header .= pack('V', $len); // uncompressed size
- $header .= pack('v', strlen($name)); // file name length
- $header .= pack('v', strlen($extra)); // extra field length
- $header .= pack('v', 0); // file comment length
- $header .= pack('v', 0); // disk number start
- $header .= pack('v', 0); // internal file attributes
- $header .= pack('V', 0); // external file attributes @todo was 0x32!?
- $header .= pack('V', $offset); // relative offset of local header
- $header .= $name; // file name
- $header .= $extra; // extra (utf-8 filename)
-
- return $header;
- }
-
- /**
- * Returns a local file header for the given data
- *
- * @param int $ts unix timestamp
- * @param int $crc CRC32 checksum of the uncompressed data
- * @param int $len length of the uncompressed data
- * @param int $clen length of the compressed data
- * @param string $name file name
- * @param boolean|null $comp if compression is used, if null it's determined from $len != $clen
- * @return string
- */
- protected function makeLocalFileHeader($ts, $crc, $len, $clen, $name, $comp = null)
- {
- if(is_null($comp)) $comp = $len != $clen;
- $comp = $comp ? 8 : 0;
- $dtime = dechex($this->makeDosTime($ts));
-
- list($name, $extra) = $this->encodeFilename($name);
-
- $header = "\x50\x4b\x03\x04"; // local file header signature
- $header .= pack('v', 20); // version needed to extract - 2.0
- $header .= pack('v', 0); // general purpose flag - no flags set
- $header .= pack('v', $comp); // compression method - deflate|none
- $header .= pack(
- 'H*',
- $dtime[6] . $dtime[7] .
- $dtime[4] . $dtime[5] .
- $dtime[2] . $dtime[3] .
- $dtime[0] . $dtime[1]
- ); // last mod file time and date
- $header .= pack('V', $crc); // crc-32
- $header .= pack('V', $clen); // compressed size
- $header .= pack('V', $len); // uncompressed size
- $header .= pack('v', strlen($name)); // file name length
- $header .= pack('v', strlen($extra)); // extra field length
- $header .= $name; // file name
- $header .= $extra; // extra (utf-8 filename)
- return $header;
- }
-
- /**
- * Returns only a part of the local file header containing the CRC, size and compressed size.
- * Used to update these fields for an already written header.
- *
- * @param int $crc CRC32 checksum of the uncompressed data
- * @param int $len length of the uncompressed data
- * @param int $clen length of the compressed data
- * @return string
- */
- protected function makeCrcAndSize($crc, $len, $clen) {
- $header = pack('V', $crc); // crc-32
- $header .= pack('V', $clen); // compressed size
- $header .= pack('V', $len); // uncompressed size
- return $header;
- }
-
- /**
- * Returns an allowed filename and an extra field header
- *
- * When encoding stuff outside the 7bit ASCII range it needs to be placed in a separate
- * extra field
- *
- * @param $original
- * @return array($filename, $extra)
- */
- protected function encodeFilename($original)
- {
- $cp437 = $this->utf8ToCp($original);
- if ($cp437 === $original) {
- return array($original, '');
- }
-
- $extra = pack(
- 'vvCV',
- 0x7075, // tag
- strlen($original) + 5, // length of file + version + crc
- 1, // version
- crc32($original) // crc
- );
- $extra .= $original;
-
- return array($cp437, $extra);
- }
-}
diff --git a/tests/FileInfoTest.php b/tests/FileInfoTest.php
deleted file mode 100644
index 95b615b..0000000
--- a/tests/FileInfoTest.php
+++ /dev/null
@@ -1,114 +0,0 @@
-assertEquals('foobar', $fileinfo->getPath());
- $this->assertTrue($fileinfo->getMtime() > time() - 30);
- $this->assertFalse($fileinfo->getIsdir());
- $this->assertEquals(0, $fileinfo->getSize());
- $this->assertEquals(0, $fileinfo->getCompressedSize());
- $this->assertEquals(0664, $fileinfo->getMode());
- $this->assertEquals(0, $fileinfo->getGid());
- $this->assertEquals(0, $fileinfo->getUid());
- $this->assertEquals('', $fileinfo->getOwner());
- $this->assertEquals('', $fileinfo->getGroup());
- $this->assertEquals('', $fileinfo->getComment());
- }
-
- public function testClean()
- {
- $data = array(
- array('foo', 'foo'),
- array('/foo/', 'foo'),
- array('/foo/../bar', 'bar'),
- array('/foo/../../bar', 'bar'),
- array('/foo/../baz/../bar', 'bar'),
- array('/foo/baz/../bar', 'foo/bar'),
- array('\\foo/baz\\../bar', 'foo/bar'),
- array('/foo/bar', 'foo/bar'),
- array('/foo/bar/', 'foo/bar'),
- array('foo//bar', 'foo/bar'),
- array('foo/0/bar', 'foo/0/bar'),
- array('foo/../bar', 'bar'),
- array('foo/bang/bang/../../bar', 'foo/bar'),
- array('foo/../../bar', 'bar'),
- array('foo/.././../bar', 'bar'),
-
- );
-
- $fileinfo = new FileInfo();
- foreach ($data as $test) {
- $fileinfo->setPath($test[0]);
- $this->assertEquals($test[1], $fileinfo->getPath());
- }
- }
-
- public function testStrip()
- {
- $fileinfo = new FileInfo('foo/bar/baz/bang');
- $this->assertEquals('foo/bar/baz/bang', $fileinfo->getPath());
-
- $fileinfo->strip(1);
- $this->assertEquals('bar/baz/bang', $fileinfo->getPath());
-
- $fileinfo->strip(2);
- $this->assertEquals('bang', $fileinfo->getPath());
-
- $fileinfo = new FileInfo('foo/bar/baz/bang');
- $fileinfo->strip('nomatch');
- $this->assertEquals('foo/bar/baz/bang', $fileinfo->getPath());
-
- $fileinfo->strip('foo/bar');
- $this->assertEquals('baz/bang', $fileinfo->getPath());
- }
-
- public function testMatchExpression()
- {
- $fileinfo = new FileInfo('foo/bar/baz/bang');
-
- $this->assertTrue($fileinfo->matchExpression());
- $this->assertTrue($fileinfo->matchExpression('/bang/'));
- $this->assertFalse($fileinfo->matchExpression('/bark/'));
-
- $this->assertFalse($fileinfo->matchExpression('', '/bang/'));
- $this->assertTrue($fileinfo->matchExpression('', '/bark/'));
-
- $this->assertFalse($fileinfo->matchExpression('/bang/', '/foo/'));
- $this->assertTrue($fileinfo->matchExpression('/bang/', '/bark/'));
- }
-
- public function testMatchDeprecation()
- {
- $this->expectException(\PHPUnit\Framework\Error\Notice::class);
- $fileinfo = new FileInfo('foo/bar/baz/bang');
- $fileinfo->match('/bang/', '/bark/');
- }
-
- public function testFromPath()
- {
- $fileinfo = FileInfo::fromPath(__DIR__ . '/zip/block.txt', 'test.txt');
- $this->assertEquals('test.txt', $fileinfo->getPath());
- $this->assertFalse($fileinfo->getIsdir());
- $this->assertSame(512, $fileinfo->getSize());
-
- $fileinfo = FileInfo::fromPath(__DIR__ . '/zip', 'zip');
- $this->assertEquals('zip', $fileinfo->getPath());
- $this->assertTrue($fileinfo->getIsdir());
- $this->assertSame(0, $fileinfo->getSize());
- }
-
- public function testFromPathWithFileNotExisted()
- {
- $this->expectException(\splitbrain\PHPArchive\FileInfoException::class);
- FileInfo::fromPath('invalid_file_path');
- }
-}
diff --git a/tests/TarTestCase.php b/tests/TarTestCase.php
deleted file mode 100644
index 1a1c5e6..0000000
--- a/tests/TarTestCase.php
+++ /dev/null
@@ -1,917 +0,0 @@
-extensions[] = 'tgz';
- $this->extensions[] = 'tar.gz';
- }
- if (extension_loaded('bz2')) {
- $this->extensions[] = 'tbz';
- $this->extensions[] = 'tar.bz2';
- }
- vfsStream::setup('home_root_path');
- }
-
- /** @inheritdoc */
- protected function tearDown(): void
- {
- parent::tearDown();
- $this->extensions[] = null;
- }
-
- /**
- * Returns the current dir with Linux style separator (/)
- *
- * This makes it easier to run the tests on Windows as well.
- *
- * @return string
- */
- protected function getDir()
- {
- return str_replace('\\', '/', __DIR__);
- }
-
- /**
- * Callback check function
- * @param FileInfo $fileinfo
- */
- public function increaseCounter($fileinfo)
- {
- $this->assertInstanceOf('\\splitbrain\\PHPArchive\\FileInfo', $fileinfo);
- $this->counter++;
- }
-
- /*
- * dependency for tests needing zlib extension to pass
- */
- public function testExtZlibIsInstalled()
- {
- $this->assertTrue(function_exists('gzopen'));
- }
-
- /*
- * dependency for tests needing bz2 extension to pass
- */
- public function testExtBz2IsInstalled()
- {
- $this->assertTrue(function_exists('bzopen'));
- }
-
- public function testTarFileIsNotExisted()
- {
- $this->expectException(ArchiveIOException::class);
- $tar = new Tar();
- $tar->open('non_existed_file.tar');
- }
-
- /**
- * simple test that checks that the given filenames and contents can be grepped from
- * the uncompressed tar stream
- *
- * No check for format correctness
- */
- public function testCreateDynamic()
- {
- $tar = new Tar();
-
- $dir = $this->getDir() . '/tar';
- $tdir = ltrim($dir, '/');
-
- $tar->create();
- $tar->addFile("$dir/testdata1.txt");
- $tar->addFile("$dir/foobar/testdata2.txt", 'noway/testdata2.txt');
- $tar->addData('another/testdata3.txt', 'testcontent3');
-
- $data = $tar->getArchive();
-
- $this->assertTrue(strpos($data, 'testcontent1') !== false, 'Content in TAR');
- $this->assertTrue(strpos($data, 'testcontent2') !== false, 'Content in TAR');
- $this->assertTrue(strpos($data, 'testcontent3') !== false, 'Content in TAR');
-
- // fullpath might be too long to be stored as full path FS#2802
- $this->assertTrue(strpos($data, "$tdir") !== false, 'Path in TAR');
- $this->assertTrue(strpos($data, "testdata1.txt") !== false, 'File in TAR');
-
- $this->assertTrue(strpos($data, 'noway/testdata2.txt') !== false, 'Path in TAR');
- $this->assertTrue(strpos($data, 'another/testdata3.txt') !== false, 'Path in TAR');
-
- // fullpath might be too long to be stored as full path FS#2802
- $this->assertTrue(strpos($data, "$tdir/foobar") === false, 'Path not in TAR');
- $this->assertTrue(strpos($data, "foobar.txt") === false, 'File not in TAR');
-
- $this->assertTrue(strpos($data, "foobar") === false, 'Path not in TAR');
- }
-
- /**
- * simple test that checks that the given filenames and contents can be grepped from the
- * uncompressed tar file
- *
- * No check for format correctness
- */
- public function testCreateFile()
- {
- $tar = new Tar();
-
- $dir = $this->getDir() . '/tar';
- $tdir = ltrim($dir, '/');
- $tmp = vfsStream::url('home_root_path/test.tar');
-
- $tar->create($tmp);
- $tar->addFile("$dir/testdata1.txt");
- $tar->addFile("$dir/foobar/testdata2.txt", 'noway/testdata2.txt');
- $tar->addData('another/testdata3.txt', 'testcontent3');
- $tar->close();
-
- $this->assertTrue(filesize($tmp) > 30); //arbitrary non-zero number
- $data = file_get_contents($tmp);
-
- $this->assertTrue(strpos($data, 'testcontent1') !== false, 'Content in TAR');
- $this->assertTrue(strpos($data, 'testcontent2') !== false, 'Content in TAR');
- $this->assertTrue(strpos($data, 'testcontent3') !== false, 'Content in TAR');
-
- // fullpath might be too long to be stored as full path FS#2802
- $this->assertTrue(strpos($data, "$tdir") !== false, "Path in TAR '$tdir'");
- $this->assertTrue(strpos($data, "testdata1.txt") !== false, 'File in TAR');
-
- $this->assertTrue(strpos($data, 'noway/testdata2.txt') !== false, 'Path in TAR');
- $this->assertTrue(strpos($data, 'another/testdata3.txt') !== false, 'Path in TAR');
-
- // fullpath might be too long to be stored as full path FS#2802
- $this->assertTrue(strpos($data, "$tdir/foobar") === false, 'Path not in TAR');
- $this->assertTrue(strpos($data, "foobar.txt") === false, 'File not in TAR');
-
- $this->assertTrue(strpos($data, "foobar") === false, 'Path not in TAR');
- }
-
- /**
- * List the contents of the prebuilt TAR files
- */
- public function testTarcontent()
- {
- $dir = $this->getDir() . '/tar';
-
- foreach ($this->extensions as $ext) {
- $tar = new Tar();
- $file = "$dir/test.$ext";
-
- $tar->open($file);
- /** @var FileInfo[] $content */
- $content = $tar->contents();
-
- $this->assertCount(4, $content, "Contents of $file");
- $this->assertEquals('tar/testdata1.txt', $content[1]->getPath(), "Contents of $file");
- $this->assertEquals(13, $content[1]->getSize(), "Contents of $file");
-
- $this->assertEquals('tar/foobar/testdata2.txt', $content[3]->getPath(), "Contents of $file");
- $this->assertEquals(13, $content[3]->getSize(), "Contents of $file");
- }
- }
-
- /**
- * Create an archive and unpack it again
- */
- public function testDogfood()
- {
- foreach ($this->extensions as $ext) {
- $input = glob($this->getDir() . '/../src/*');
- $archive = sys_get_temp_dir() . '/dwtartest' . md5(time()) . '.' . $ext;
- $extract = sys_get_temp_dir() . '/dwtartest' . md5(time() + 1);
-
- $this->counter = 0;
- $tar = new Tar();
- $tar->setCallback(array($this, 'increaseCounter'));
- $tar->create($archive);
- foreach ($input as $path) {
- $file = basename($path);
- $tar->addFile($path, $file);
- }
- $tar->close();
- $this->assertFileExists($archive);
- $this->assertEquals(count($input), $this->counter);
-
- $this->counter = 0;
- $tar = new Tar();
- $tar->setCallback(array($this, 'increaseCounter'));
- $tar->open($archive);
- $tar->extract($extract, '', '/FileInfo\\.php/', '/.*\\.php/');
-
- $this->assertFileExists("$extract/Tar.php");
- $this->assertFileExists("$extract/Zip.php");
- $this->assertFileNotExists("$extract/FileInfo.php");
-
- $this->assertEquals(count($input) - 1, $this->counter);
-
- $this->nativeCheck($archive, $ext);
-
- self::RDelete($extract);
- unlink($archive);
- }
- }
-
- /**
- * Test the given archive with a native tar installation (if available)
- *
- * @param $archive
- * @param $ext
- */
- protected function nativeCheck($archive, $ext)
- {
- if (!is_executable('/usr/bin/tar')) {
- return;
- }
-
- $switch = array(
- 'tar' => '-tf',
- 'tgz' => '-tzf',
- 'tar.gz' => '-tzf',
- 'tbz' => '-tjf',
- 'tar.bz2' => '-tjf',
- );
- $arg = $switch[$ext];
- $archive = escapeshellarg($archive);
-
- $return = 0;
- $output = array();
- $ok = exec("/usr/bin/tar $arg $archive 2>&1 >/dev/null", $output, $return);
- $output = join("\n", $output);
-
- $this->assertNotFalse($ok, "native tar execution for $archive failed:\n$output");
- $this->assertSame(0, $return, "native tar execution for $archive had non-zero exit code $return:\n$output");
- $this->assertSame('', $output, "native tar execution for $archive had non-empty output:\n$output");
- }
-
- /**
- * Extract the prebuilt tar files
- */
- public function testTarExtract()
- {
- $dir = $this->getDir() . '/tar';
- $out = sys_get_temp_dir() . '/dwtartest' . md5(time());
-
- foreach ($this->extensions as $ext) {
- $tar = new Tar();
- $file = "$dir/test.$ext";
-
- $tar->open($file);
- $tar->extract($out);
-
- clearstatcache();
-
- $this->assertFileExists($out . '/tar/testdata1.txt', "Extracted $file");
- $this->assertEquals(13, filesize($out . '/tar/testdata1.txt'), "Extracted $file");
-
- $this->assertFileExists($out . '/tar/foobar/testdata2.txt', "Extracted $file");
- $this->assertEquals(13, filesize($out . '/tar/foobar/testdata2.txt'), "Extracted $file");
-
- self::RDelete($out);
- }
- }
-
- /**
- * Extract the prebuilt tar files with component stripping
- */
- public function testCompStripExtract()
- {
- $dir = $this->getDir() . '/tar';
- $out = sys_get_temp_dir() . '/dwtartest' . md5(time());
-
- foreach ($this->extensions as $ext) {
- $tar = new Tar();
- $file = "$dir/test.$ext";
-
- $tar->open($file);
- $tar->extract($out, 1);
-
- clearstatcache();
-
- $this->assertFileExists($out . '/testdata1.txt', "Extracted $file");
- $this->assertEquals(13, filesize($out . '/testdata1.txt'), "Extracted $file");
-
- $this->assertFileExists($out . '/foobar/testdata2.txt', "Extracted $file");
- $this->assertEquals(13, filesize($out . '/foobar/testdata2.txt'), "Extracted $file");
-
- self::RDelete($out);
- }
- }
-
- /**
- * Extract the prebuilt tar files with prefix stripping
- */
- public function testPrefixStripExtract()
- {
- $dir = $this->getDir() . '/tar';
- $out = sys_get_temp_dir() . '/dwtartest' . md5(time());
-
- foreach ($this->extensions as $ext) {
- $tar = new Tar();
- $file = "$dir/test.$ext";
-
- $tar->open($file);
- $tar->extract($out, 'tar/foobar/');
-
- clearstatcache();
-
- $this->assertFileExists($out . '/tar/testdata1.txt', "Extracted $file");
- $this->assertEquals(13, filesize($out . '/tar/testdata1.txt'), "Extracted $file");
-
- $this->assertFileExists($out . '/testdata2.txt', "Extracted $file");
- $this->assertEquals(13, filesize($out . '/testdata2.txt'), "Extracted $file");
-
- self::RDelete($out);
- }
- }
-
- /**
- * Extract the prebuilt tar files with include regex
- */
- public function testIncludeExtract()
- {
- $dir = $this->getDir() . '/tar';
- $out = sys_get_temp_dir() . '/dwtartest' . md5(time());
-
- foreach ($this->extensions as $ext) {
- $tar = new Tar();
- $file = "$dir/test.$ext";
-
- $tar->open($file);
- $tar->extract($out, '', '', '/\/foobar\//');
-
- clearstatcache();
-
- $this->assertFileNotExists($out . '/tar/testdata1.txt', "Extracted $file");
-
- $this->assertFileExists($out . '/tar/foobar/testdata2.txt', "Extracted $file");
- $this->assertEquals(13, filesize($out . '/tar/foobar/testdata2.txt'), "Extracted $file");
-
- self::RDelete($out);
- }
- }
-
- /**
- * Extract the prebuilt tar files with exclude regex
- */
- public function testExcludeExtract()
- {
- $dir = $this->getDir() . '/tar';
- $out = sys_get_temp_dir() . '/dwtartest' . md5(time());
-
- foreach ($this->extensions as $ext) {
- $tar = new Tar();
- $file = "$dir/test.$ext";
-
- $tar->open($file);
- $tar->extract($out, '', '/\/foobar\//');
-
- clearstatcache();
-
- $this->assertFileExists($out . '/tar/testdata1.txt', "Extracted $file");
- $this->assertEquals(13, filesize($out . '/tar/testdata1.txt'), "Extracted $file");
-
- $this->assertFileNotExists($out . '/tar/foobar/testdata2.txt', "Extracted $file");
-
- self::RDelete($out);
- }
- }
-
- /**
- * Check the extension to compression guesser
- */
- public function testFileType()
- {
- $tar = new Tar();
- $this->assertEquals(Tar::COMPRESS_NONE, $tar->filetype('foo'));
- $this->assertEquals(Tar::COMPRESS_GZIP, $tar->filetype('foo.tgz'));
- $this->assertEquals(Tar::COMPRESS_GZIP, $tar->filetype('foo.tGZ'));
- $this->assertEquals(Tar::COMPRESS_GZIP, $tar->filetype('foo.tar.GZ'));
- $this->assertEquals(Tar::COMPRESS_GZIP, $tar->filetype('foo.tar.gz'));
- $this->assertEquals(Tar::COMPRESS_BZIP, $tar->filetype('foo.tbz'));
- $this->assertEquals(Tar::COMPRESS_BZIP, $tar->filetype('foo.tBZ'));
- $this->assertEquals(Tar::COMPRESS_BZIP, $tar->filetype('foo.tar.BZ2'));
- $this->assertEquals(Tar::COMPRESS_BZIP, $tar->filetype('foo.tar.bz2'));
-
- $dir = $this->getDir() . '/tar';
- $this->assertEquals(Tar::COMPRESS_NONE, $tar->filetype("$dir/test.tar"));
- $this->assertEquals(Tar::COMPRESS_GZIP, $tar->filetype("$dir/test.tgz"));
- $this->assertEquals(Tar::COMPRESS_BZIP, $tar->filetype("$dir/test.tbz"));
- $this->assertEquals(Tar::COMPRESS_NONE, $tar->filetype("$dir/test.tar.guess"));
- $this->assertEquals(Tar::COMPRESS_GZIP, $tar->filetype("$dir/test.tgz.guess"));
- $this->assertEquals(Tar::COMPRESS_BZIP, $tar->filetype("$dir/test.tbz.guess"));
- }
-
- /**
- * @depends testExtZlibIsInstalled
- */
- public function testLongPathExtract()
- {
- $dir = $this->getDir() . '/tar';
- $out = vfsStream::url('home_root_path/dwtartest' . md5(time()));
-
- foreach (array('ustar', 'gnu') as $format) {
- $tar = new Tar();
- $tar->open("$dir/longpath-$format.tgz");
- $tar->extract($out);
-
- $this->assertFileExists(
- $out . '/1234567890/1234567890/1234567890/1234567890/1234567890/1234567890/1234567890/1234567890/1234567890/1234567890/1234567890/1234567890/test.txt'
- );
- }
- }
-
- // FS#1442
- public function testCreateLongFile()
- {
- $tar = new Tar();
- $tar->setCompression(0);
- $tmp = vfsStream::url('home_root_path/dwtartest');
-
- $path = '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789.txt';
-
- $tar->create($tmp);
- $tar->addData($path, 'testcontent1');
- $tar->close();
-
- $this->assertTrue(filesize($tmp) > 30); //arbitrary non-zero number
- $data = file_get_contents($tmp);
-
- // We should find the complete path and a longlink entry
- $this->assertTrue(strpos($data, 'testcontent1') !== false, 'content in TAR');
- $this->assertTrue(strpos($data, $path) !== false, 'path in TAR');
- $this->assertTrue(strpos($data, '@LongLink') !== false, '@LongLink in TAR');
- }
-
- public function testCreateLongPathTar()
- {
- $tar = new Tar();
- $tar->setCompression(0);
- $tmp = vfsStream::url('home_root_path/dwtartest');
-
- $path = '';
- for ($i = 0; $i < 11; $i++) {
- $path .= '1234567890/';
- }
- $path = rtrim($path, '/');
-
- $tar->create($tmp);
- $tar->addData("$path/test.txt", 'testcontent1');
- $tar->close();
-
- $this->assertTrue(filesize($tmp) > 30); //arbitrary non-zero number
- $data = file_get_contents($tmp);
-
- // We should find the path and filename separated, no longlink entry
- $this->assertTrue(strpos($data, 'testcontent1') !== false, 'content in TAR');
- $this->assertTrue(strpos($data, 'test.txt') !== false, 'filename in TAR');
- $this->assertTrue(strpos($data, $path) !== false, 'path in TAR');
- $this->assertFalse(strpos($data, "$path/test.txt") !== false, 'full filename in TAR');
- $this->assertFalse(strpos($data, '@LongLink') !== false, '@LongLink in TAR');
- }
-
- public function testCreateLongPathGnu()
- {
- $tar = new Tar();
- $tar->setCompression(0);
- $tmp = vfsStream::url('home_root_path/dwtartest');
-
- $path = '';
- for ($i = 0; $i < 20; $i++) {
- $path .= '1234567890/';
- }
- $path = rtrim($path, '/');
-
- $tar->create($tmp);
- $tar->addData("$path/test.txt", 'testcontent1');
- $tar->close();
-
- $this->assertTrue(filesize($tmp) > 30); //arbitrary non-zero number
- $data = file_get_contents($tmp);
-
- // We should find the complete path/filename and a longlink entry
- $this->assertTrue(strpos($data, 'testcontent1') !== false, 'content in TAR');
- $this->assertTrue(strpos($data, 'test.txt') !== false, 'filename in TAR');
- $this->assertTrue(strpos($data, $path) !== false, 'path in TAR');
- $this->assertTrue(strpos($data, "$path/test.txt") !== false, 'full filename in TAR');
- $this->assertTrue(strpos($data, '@LongLink') !== false, '@LongLink in TAR');
- }
-
- /**
- * Extract a tarbomomb
- * @depends testExtZlibIsInstalled
- */
- public function testTarBomb()
- {
- $dir = $this->getDir() . '/tar';
- $out = vfsStream::url('home_root_path/dwtartest' . md5(time()));
-
- $tar = new Tar();
-
- $tar->open("$dir/tarbomb.tgz");
- $tar->extract($out);
-
- clearstatcache();
-
- $this->assertFileExists(
- $out . '/AAAAAAAAAAAAAAAAA/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB.txt'
- );
- }
-
- /**
- * A single zero file should be just a header block + the footer
- */
- public function testZeroFile()
- {
- $dir = $this->getDir() . '/tar';
- $tar = new Tar();
- $tar->setCompression(0);
- $tar->create();
- $tar->addFile("$dir/zero.txt", 'zero.txt');
- $file = $tar->getArchive();
-
- $this->assertEquals(512 * 3, strlen($file)); // 1 header block + 2 footer blocks
- }
-
- public function testZeroData()
- {
- $tar = new Tar();
- $tar->setCompression(0);
- $tar->create();
- $tar->addData('zero.txt', '');
- $file = $tar->getArchive();
-
- $this->assertEquals(512 * 3, strlen($file)); // 1 header block + 2 footer blocks
- }
-
- /**
- * Add a zero byte file to a tar and extract it again
- */
- public function testZeroByteFile()
- {
- $archive = sys_get_temp_dir() . '/dwziptest' . md5(time()) . '.zip';
- $extract = sys_get_temp_dir() . '/dwziptest' . md5(time() + 1);
-
- $tar = new Tar();
- $tar->create($archive);
- $tar->addFile($this->getDir() . '/zip/zero.txt', 'foo/zero.txt');
- $tar->close();
- $this->assertFileExists($archive);
-
- $tar = new Tar();
- $tar->open($archive);
- $contents = $tar->contents();
-
- $this->assertEquals(1, count($contents));
- $this->assertEquals('foo/zero.txt', ($contents[0])->getPath());
-
- $tar = new Tar();
- $tar->open($archive);
- $tar->extract($extract);
- $tar->close();
-
- $this->assertFileExists("$extract/foo/zero.txt");
- $this->assertEquals(0, filesize("$extract/foo/zero.txt"));
-
- self::RDelete($extract);
- unlink($archive);
- }
-
- /**
- * A file of exactly one block should be just a header block + data block + the footer
- */
- public function testBlockFile()
- {
- $dir = $this->getDir() . '/tar';
- $tar = new Tar();
- $tar->setCompression(0);
- $tar->create();
- $tar->addFile("$dir/block.txt", 'block.txt');
- $file = $tar->getArchive();
-
- $this->assertEquals(512 * 4, strlen($file)); // 1 header block + data block + 2 footer blocks
- }
-
- public function testBlockData()
- {
- $tar = new Tar();
- $tar->setCompression(0);
- $tar->create();
- $tar->addData('block.txt', str_pad('', 512, 'x'));
- $file = $tar->getArchive();
-
- $this->assertEquals(512 * 4, strlen($file)); // 1 header block + data block + 2 footer blocks
- }
-
- /**
- * @depends testExtZlibIsInstalled
- */
- public function testGzipIsValid()
- {
- foreach (['tgz', 'tar.gz'] as $ext) {
- $input = glob($this->getDir() . '/../src/*');
- $archive = sys_get_temp_dir() . '/dwtartest' . md5(time()) . '.' . $ext;
- $extract = sys_get_temp_dir() . '/dwtartest' . md5(time() + 1);
-
- $tar = new Tar();
- $tar->setCompression(9, Tar::COMPRESS_GZIP);
- $tar->create();
- foreach ($input as $path) {
- $file = basename($path);
- $tar->addFile($path, $file);
- }
- $tar->save($archive);
- $this->assertFileExists($archive);
-
- try {
- $phar = new \PharData($archive);
- $phar->extractTo($extract);
- } catch(\Exception $e) {
- };
-
- $this->assertFileExists("$extract/Tar.php");
- $this->assertFileExists("$extract/Zip.php");
-
- $this->nativeCheck($archive, $ext);
-
- self::RDelete($extract);
- unlink($archive);
- }
- }
-
- public function testContentsWithInvalidArchiveStream()
- {
- $this->expectException(ArchiveIOException::class);
- $tar = new Tar();
- $tar->contents();
- }
-
- public function testExtractWithInvalidOutDir()
- {
- $this->expectException(ArchiveIOException::class);
- $dir = $this->getDir() . '/tar';
- // Fails on Linux and Windows.
- $out = '/root/invalid_out_dir:';
-
- $tar = new Tar();
-
- $tar->open("$dir/tarbomb.tgz");
- $tar->extract($out);
- }
-
- public function testExtractWithArchiveStreamIsClosed()
- {
- $this->expectException(ArchiveIOException::class);
- $dir = $this->getDir() . '/tar';
- $out = '/root/invalid_out_dir';
-
- $tar = new Tar();
-
- $tar->open("$dir/tarbomb.tgz");
- $tar->close();
- $tar->extract($out);
- }
-
- public function testCreateWithInvalidFile()
- {
- $this->expectException(ArchiveIOException::class);
- $dir = $this->getDir() . '/tar';
- $tar = new Tar();
-
- $tar->open("$dir/tarbomb.tgz");
- $tar->create('/root/invalid_file:');
- }
-
- public function testAddFileWithArchiveStreamIsClosed()
- {
- $this->expectException(ArchiveIOException::class);
- $archive = sys_get_temp_dir() . '/dwtartest' . md5(time()) . '.tar';
-
- $tar = new Tar();
- $tar->create($archive);
- $tar->close();
- $tar->addFile('archive_file', false);
- }
-
- public function testAddFileWithInvalidFile()
- {
- $this->expectException(FileInfoException::class);
- $archive = sys_get_temp_dir() . '/dwtartest' . md5(time()) . '.tar';
-
- $tar = new Tar();
- $tar->create($archive);
- $tar->addFile('archive_file', 'a-non-existing-file.txt');
- }
-
- public function testAddDataWithArchiveStreamIsClosed()
- {
- $this->expectException(ArchiveIOException::class);
- $archive = sys_get_temp_dir() . '/dwtartest' . md5(time()) . '.tar';
-
- $tar = new Tar();
- $tar->create($archive);
- $tar->close();
- $tar->addData(false, '');
- }
-
- public function testCloseHasBeenClosed()
- {
- $archive = sys_get_temp_dir() . '/dwtartest' . md5(time()) . '.tar';
-
- $tar = new Tar();
- $tar->create($archive);
- $tar->close();
-
- $tar->close();
- $this->assertTrue(true); // succeed if no exception, yet
- }
-
- /**
- * @depends testExtBz2IsInstalled
- */
- public function testGetArchiveWithBzipCompress()
- {
- $dir = $this->getDir() . '/tar';
- $tar = new Tar();
- $tar->setCompression(9, Tar::COMPRESS_BZIP);
- $tar->create();
- $tar->addFile("$dir/zero.txt", 'zero.txt');
- $file = $tar->getArchive();
-
- $this->assertIsString($file); // 1 header block + 2 footer blocks
- }
-
- public function testSaveWithCompressionAuto()
- {
- $dir = $this->getDir() . '/tar';
- $tar = new Tar();
- $tar->setCompression(-1);
- $tar->create();
- $tar->addFile("$dir/zero.txt", 'zero.txt');
-
- $tar->save(vfsStream::url('home_root_path/archive_file'));
- $this->assertTrue(true); // succeed if no exception, yet
- }
-
- public function testSaveWithInvalidDestinationFile()
- {
- $this->expectException(ArchiveIOException::class);
- $dir = $this->getDir() . '/tar';
- $tar = new Tar();
- $tar->setCompression();
- $tar->create();
- $tar->addFile("$dir/zero.txt", 'zero.txt');
-
- $tar->save(vfsStream::url('archive_file'));
- $this->assertTrue(true); // succeed if no exception, yet
- }
-
- public function testNumberEncodeDecode()
- {
- // 2^34 + 17 = 2^2 * 2^32 + 17
- $refValue = (1 << 34) + 17;
- $encoded = Tar::numberEncode($refValue, 12);
- $this->assertEquals(pack('CCnNN', 128, 0, 0, 1 << 2, 17), $encoded);
- $decoded = Tar::numberDecode($encoded);
- $this->assertEquals($refValue, $decoded);
-
- $encoded = Tar::numberEncode($refValue, 7);
- $this->assertEquals(pack('CnN', 128, 1 << 2, 17), $encoded);
- $decoded = Tar::numberDecode($encoded);
- $this->assertEquals($refValue, $decoded);
-
- $refValue = -1234;
- $encoded = Tar::numberEncode($refValue, 12);
- $this->assertEquals(pack('CCnNN', 0xFF, 0xFF, 0xFFFF, 0xFFFFFFFF, -1234), $encoded);
- $decoded = Tar::numberDecode($encoded);
- $this->assertEquals($refValue, $decoded);
-
- $encoded = Tar::numberEncode($refValue, 3);
- $this->assertEquals(pack('Cn', 0xFF, -1234), $encoded);
- $decoded = Tar::numberDecode($encoded);
- $this->assertEquals($refValue, $decoded);
- }
-
- public function testReadCurrentEntry()
- {
- $tar = new Tar();
- $tar->open(__DIR__ . '/tar/test.tar');
- $out = sys_get_temp_dir() . '/dwtartest' . md5(time());
- $tar->extract($out);
-
- $tar = new Tar();
- $tar->open(__DIR__ . '/tar/test.tar');
- $pathsRead = array();
- foreach ($tar->yieldContents() as $i) {
- $this->assertFileExists($out . '/' . $i->getPath());
- if ($i->getIsdir()) {
- $this->assertEquals('', $tar->readCurrentEntry());
- } else {
- $this->assertStringEqualsFile($out . '/' . $i->getPath(), $tar->readCurrentEntry());
- }
- $pathsRead[] = $i->getPath();
- }
- $pathsReadRef = array('tar', 'tar/testdata1.txt', 'tar/foobar', 'tar/foobar/testdata2.txt');
- $this->assertEquals($pathsReadRef, $pathsRead);
-
- self::RDelete($out);
- }
-
- /**
- * Create an archive, extract it, and compare file properties
- */
- public function testFilePropertiesPreservation()
- {
- $input = glob($this->getDir() . '/../src/*');
- $archive = sys_get_temp_dir() . '/dwtartest' . md5(time()) . '.tar';
- $extract = sys_get_temp_dir() . '/dwtartest' . md5(time() + 1);
-
- // Create archive
- $tar = new Tar();
- $tar->create($archive);
- foreach ($input as $path) {
- $file = basename($path);
- $tar->addFile($path, $file);
- }
- $tar->close();
- $this->assertFileExists($archive);
-
- // Extract archive
- $tar = new Tar();
- $tar->open($archive);
- $tar->extract($extract);
- $tar->close();
-
- // Compare file properties
- foreach ($input as $originalPath) {
- $filename = basename($originalPath);
- $extractedPath = $extract . '/' . $filename;
-
- $this->assertFileExists($extractedPath, "Extracted file should exist: $filename");
-
- // Compare file sizes
- $originalSize = filesize($originalPath);
- $extractedSize = filesize($extractedPath);
- $this->assertEquals($originalSize, $extractedSize, "File size should match for: $filename");
-
- // Compare file contents
- $originalContent = file_get_contents($originalPath);
- $extractedContent = file_get_contents($extractedPath);
- $this->assertEquals($originalContent, $extractedContent, "File content should match for: $filename");
-
- // Compare modification times (allow small difference due to tar format limitations)
- $originalMtime = filemtime($originalPath);
- $extractedMtime = filemtime($extractedPath);
- $this->assertLessThanOrEqual(1, abs($originalMtime - $extractedMtime),
- "Modification time should be preserved (within 1 second) for: $filename");
-
- // Compare file permissions (only on Unix-like systems)
- if (DIRECTORY_SEPARATOR === '/') {
- $originalPerms = fileperms($originalPath) & 0777;
- $extractedPerms = fileperms($extractedPath) & 0777;
- $this->assertEquals($originalPerms, $extractedPerms,
- "File permissions should match for: $filename");
- }
- }
-
- self::RDelete($extract);
- unlink($archive);
- }
-
- /**
- * recursive rmdir()/unlink()
- *
- * @static
- * @param $target string
- */
- public static function RDelete($target)
- {
- if (!is_dir($target)) {
- unlink($target);
- } else {
- $dh = dir($target);
- while (false !== ($entry = $dh->read())) {
- if ($entry == '.' || $entry == '..') {
- continue;
- }
- self::RDelete("$target/$entry");
- }
- $dh->close();
- rmdir($target);
- }
- }
-}
diff --git a/tests/ZipTestCase.php b/tests/ZipTestCase.php
deleted file mode 100644
index 7d4f7a5..0000000
--- a/tests/ZipTestCase.php
+++ /dev/null
@@ -1,647 +0,0 @@
-assertInstanceOf('\\splitbrain\\PHPArchive\\FileInfo', $fileinfo);
- $this->counter++;
- }
-
- /*
- * dependency for tests needing zip extension to pass
- */
- public function testExtZipIsInstalled()
- {
- $this->assertTrue(function_exists('zip_open'));
- }
-
- public function testMissing()
- {
- $this->expectException(ArchiveIOException::class);
- $tar = new Zip();
- $tar->open('nope.zip');
- }
-
- /**
- * simple test that checks that the given filenames and contents can be grepped from
- * the uncompressed zip stream
- *
- * No check for format correctness
- * @depends testExtZipIsInstalled
- */
- public function testCreateDynamic()
- {
- $zip = new Zip();
-
- $dir = $this->getDir() . '/zip';
- $tdir = ltrim($dir, '/');
-
- $zip->create();
- $zip->setCompression(0);
- $zip->AddFile("$dir/testdata1.txt", "$dir/testdata1.txt");
- $zip->AddFile("$dir/foobar/testdata2.txt", 'noway/testdata2.txt');
- $zip->addData('another/testdata3.txt', 'testcontent3');
-
- $data = $zip->getArchive();
-
- $this->assertTrue(strpos($data, 'testcontent1') !== false, 'Content 1 in ZIP');
- $this->assertTrue(strpos($data, 'testcontent2') !== false, 'Content 2 in ZIP');
- $this->assertTrue(strpos($data, 'testcontent3') !== false, 'Content 3 in ZIP');
-
- // fullpath might be too long to be stored as full path FS#2802
- $this->assertTrue(strpos($data, "$tdir") !== false, 'Path in ZIP');
- $this->assertTrue(strpos($data, "testdata1.txt") !== false, 'File in ZIP');
-
- $this->assertTrue(strpos($data, 'noway/testdata2.txt') !== false, 'Path in ZIP');
- $this->assertTrue(strpos($data, 'another/testdata3.txt') !== false, 'Path in ZIP');
-
- // fullpath might be too long to be stored as full path FS#2802
- $this->assertTrue(strpos($data, "$tdir/foobar") === false, 'Path not in ZIP');
- $this->assertTrue(strpos($data, "foobar.txt") === false, 'File not in ZIP');
-
- $this->assertTrue(strpos($data, "foobar") === false, 'Path not in ZIP');
- }
-
- /**
- * simple test that checks that the given filenames and contents can be grepped from the
- * uncompressed zip file
- *
- * No check for format correctness
- * @depends testExtZipIsInstalled
- */
- public function testCreateFile()
- {
- $zip = new Zip();
-
- $dir = $this->getDir() . '/zip';
- $tdir = ltrim($dir, '/');
- $tmp = vfsStream::url('home_root_path/test.zip');
-
- $zip->create($tmp);
- $zip->setCompression(0);
- $zip->addFile("$dir/testdata1.txt", "$dir/testdata1.txt");
- $zip->addFile("$dir/foobar/testdata2.txt", 'noway/testdata2.txt');
- $zip->addData('another/testdata3.txt', 'testcontent3');
- $zip->close();
-
- $this->assertTrue(filesize($tmp) > 30); //arbitrary non-zero number
- $data = file_get_contents($tmp);
-
- $this->assertTrue(strpos($data, 'testcontent1') !== false, 'Content in ZIP');
- $this->assertTrue(strpos($data, 'testcontent2') !== false, 'Content in ZIP');
- $this->assertTrue(strpos($data, 'testcontent3') !== false, 'Content in ZIP');
-
- // fullpath might be too long to be stored as full path FS#2802
- $this->assertTrue(strpos($data, "$tdir") !== false, "Path in ZIP '$tdir'");
- $this->assertTrue(strpos($data, "testdata1.txt") !== false, 'File in ZIP');
-
- $this->assertTrue(strpos($data, 'noway/testdata2.txt') !== false, 'Path in ZIP');
- $this->assertTrue(strpos($data, 'another/testdata3.txt') !== false, 'Path in ZIP');
-
- // fullpath might be too long to be stored as full path FS#2802
- $this->assertTrue(strpos($data, "$tdir/foobar") === false, 'Path not in ZIP');
- $this->assertTrue(strpos($data, "foobar.txt") === false, 'File not in ZIP');
-
- $this->assertTrue(strpos($data, "foobar") === false, 'Path not in ZIP');
- }
-
- public function testCreateWithInvalidFilePath()
- {
- $this->expectException(ArchiveIOException::class);
- $zip = new Zip();
- $tmp = vfsStream::url('invalid_root_path/test.zip');
- $zip->create($tmp);
- }
-
- public function testAddFileWithArchiveStreamIsClosed()
- {
- $this->expectException(ArchiveIOException::class);
- $zip = new Zip();
- $dir = $this->getDir() . '/zip';
- $zip->setCompression(0);
- $zip->close();
- $zip->addFile("$dir/testdata1.txt", "$dir/testdata1.txt");
- }
-
- public function testAddFileWithInvalidFile()
- {
- $this->expectException(ArchiveIOException::class);
- $zip = new Zip();
- $tmp = vfsStream::url('home_root_path/test.zip');
- $zip->create($tmp);
- $zip->setCompression(0);
- $zip->addFile('invalid_file', false);
- $zip->close();
- }
-
- /**
- * List the contents of the prebuilt ZIP file
- * @depends testExtZipIsInstalled
- */
- public function testZipContent()
- {
- $dir = $this->getDir() . '/zip';
-
- $zip = new Zip();
- $file = "$dir/test.zip";
-
- $zip->open($file);
- $content = $zip->contents();
-
- $this->assertCount(5, $content, "Contents of $file");
- $this->assertEquals('zip/testdata1.txt', $content[2]->getPath(), "Contents of $file");
- $this->assertEquals(13, $content[2]->getSize(), "Contents of $file");
-
- $this->assertEquals('zip/foobar/testdata2.txt', $content[4]->getPath(), "Contents of $file");
- $this->assertEquals(13, $content[4]->getSize(), "Contents of $file");
- }
-
- public function testZipContentWithArchiveStreamIsClosed()
- {
- $this->expectException(ArchiveIOException::class);
- $dir = $this->getDir() . '/zip';
- $zip = new Zip();
- $file = "$dir/test.zip";
- $zip->open($file);
- $zip->close();
- $zip->contents();
- }
-
- /**
- * Create an archive and unpack it again
- * @depends testExtZipIsInstalled
- */
- public function testDogFood()
- {
- $input = glob($this->getDir() . '/../src/*');
- $archive = sys_get_temp_dir() . '/dwziptest' . md5(time()) . '.zip';
- $extract = sys_get_temp_dir() . '/dwziptest' . md5(time() + 1);
-
- $this->counter = 0;
- $zip = new Zip();
- $zip->setCallback(array($this, 'increaseCounter'));
- $zip->create($archive);
- foreach ($input as $path) {
- $file = basename($path);
- $zip->addFile($path, $file);
- }
- $zip->close();
- $this->assertFileExists($archive);
- $this->assertEquals(count($input), $this->counter);
-
- $this->counter = 0;
- $zip = new Zip();
- $zip->setCallback(array($this, 'increaseCounter'));
- $zip->open($archive);
- $zip->extract($extract, '', '/FileInfo\\.php/', '/.*\\.php/');
-
- $this->assertFileExists("$extract/Tar.php");
- $this->assertFileExists("$extract/Zip.php");
- $this->assertFileNotExists("$extract/FileInfo.php");
-
- $this->assertEquals(count($input) - 1, $this->counter);
-
- $this->nativeCheck($archive);
- $this->native7ZipCheck($archive);
-
- self::RDelete($extract);
- unlink($archive);
- }
-
- /**
- * Add a zero byte file to a zip and extract it again
- */
- public function testZeroByteFile() {
- $archive = sys_get_temp_dir() . '/dwziptest' . md5(time()) . '.zip';
- $extract = sys_get_temp_dir() . '/dwziptest' . md5(time() + 1);
-
- $zip = new Zip();
- $zip->create($archive);
- $zip->addFile($this->getDir() . '/zip/zero.txt', 'foo/zero.txt');
- $zip->close();
- $this->assertFileExists($archive);
-
- $zip = new Zip();
- $zip->open($archive);
- $contents = $zip->contents();
-
- $this->assertEquals(1, count($contents));
- $this->assertEquals('foo/zero.txt', ($contents[0])->getPath());
-
- $zip = new Zip();
- $zip->open($archive);
- $zip->extract($extract);
- $zip->close();
-
- $this->assertFileExists("$extract/foo/zero.txt");
- $this->assertEquals(0, filesize("$extract/foo/zero.txt"));
-
- self::RDelete($extract);
- unlink($archive);
- }
-
- /**
- * @depends testExtZipIsInstalled
- */
- public function testUtf8()
- {
- $archive = sys_get_temp_dir() . '/dwziptest' . md5(time()) . '.zip';
- $extract = sys_get_temp_dir() . '/dwziptest' . md5(time() + 1);
-
- $zip = new Zip();
- $zip->create($archive);
- $zip->addData('tüst.txt', 'test');
- $zip->addData('snowy☃.txt', 'test');
- $zip->close();
- $this->assertFileExists($archive);
-
- $zip = new Zip();
- $zip->open($archive);
- $zip->extract($extract);
-
- $this->assertFileExists($extract . '/tüst.txt');
- $this->assertFileExists($extract . '/snowy☃.txt');
-
- $this->nativeCheck($archive);
- $this->native7ZipCheck($archive);
-
- self::RDelete($extract);
- unlink($archive);
- }
-
- public function testAddDataWithArchiveStreamIsClosed()
- {
- $this->expectException(ArchiveIOException::class);
- $archive = sys_get_temp_dir() . '/dwziptest' . md5(time()) . '.zip';
-
- $zip = new Zip();
- $zip->create($archive);
- $zip->close();
- $zip->addData('tüst.txt', 'test');
- }
-
- public function testCloseWithArchiveStreamIsClosed()
- {
- $archive = sys_get_temp_dir() . '/dwziptest' . md5(time()) . '.zip';
-
- $zip = new Zip();
- $zip->create($archive);
- $zip->close();
-
- $zip->close();
- $this->assertTrue(true); // succeed if no exception, yet
- }
-
- public function testSaveArchiveFile()
- {
- $dir = $this->getDir() . '/tar';
- $zip = new zip();
- $zip->setCompression(-1);
- $zip->create();
- $zip->addFile("$dir/zero.txt", 'zero.txt');
-
- $zip->save(vfsStream::url('home_root_path/archive_file'));
- $this->assertTrue(true); // succeed if no exception, yet
- }
-
- public function testSaveWithInvalidFilePath()
- {
- $this->expectException(ArchiveIOException::class);
- $archive = sys_get_temp_dir() . '/dwziptest' . md5(time()) . '.zip';
-
- $zip = new Zip();
- $zip->create($archive);
- $zip->save(vfsStream::url('invalid_root_path/save.zip'));
- }
-
- /**
- * Test the given archive with a native zip installation (if available)
- *
- * @param $archive
- */
- protected function nativeCheck($archive)
- {
- if (!is_executable('/usr/bin/zipinfo')) {
- return;
- }
- $archive = escapeshellarg($archive);
-
- $return = 0;
- $output = array();
- $ok = exec("/usr/bin/zipinfo $archive 2>&1 >/dev/null", $output, $return);
- $output = join("\n", $output);
-
- $this->assertNotFalse($ok, "native zip execution for $archive failed:\n$output");
- $this->assertSame(0, $return, "native zip execution for $archive had non-zero exit code $return:\n$output");
- $this->assertSame('', $output, "native zip execution for $archive had non-empty output:\n$output");
- }
-
- /**
- * Test the given archive with a native 7zip installation (if available)
- *
- * @param $archive
- */
- protected function native7ZipCheck($archive)
- {
- if (!is_executable('/usr/bin/7z')) {
- return;
- }
- $archive = escapeshellarg($archive);
-
- $return = 0;
- $output = array();
- $ok = exec("/usr/bin/7z t $archive 2>&1 >/dev/null", $output, $return);
- $output = join("\n", $output);
-
- $this->assertNotFalse($ok, "native 7zip execution for $archive failed:\n$output");
- $this->assertSame(0, $return, "native 7zip execution for $archive had non-zero exit code $return:\n$output");
- $this->assertSame('', $output, "native 7zip execution for $archive had non-empty output:\n$output");
- }
-
- /**
- * Extract the prebuilt zip files
- * @depends testExtZipIsInstalled
- */
- public function testZipExtract()
- {
- $dir = $this->getDir() . '/zip';
- $out = sys_get_temp_dir() . '/dwziptest' . md5(time());
-
- $zip = new Zip();
- $file = "$dir/test.zip";
-
- $zip->open($file);
- $zip->extract($out);
-
- clearstatcache();
-
- $this->assertFileExists($out . '/zip/testdata1.txt', "Extracted $file");
- $this->assertEquals(13, filesize($out . '/zip/testdata1.txt'), "Extracted $file");
-
- $this->assertFileExists($out . '/zip/foobar/testdata2.txt', "Extracted $file");
- $this->assertEquals(13, filesize($out . '/zip/foobar/testdata2.txt'), "Extracted $file");
-
- $this->assertFileExists($out . '/zip/compressable.txt', "Extracted $file");
- $this->assertEquals(1836, filesize($out . '/zip/compressable.txt'), "Extracted $file");
- $this->assertFileNotExists($out . '/zip/compressable.txt.gz', "Extracted $file");
-
- self::RDelete($out);
- }
-
- public function testZipExtractWithArchiveStreamIsClosed()
- {
- $this->expectException(ArchiveIOException::class);
- $dir = $this->getDir() . '/zip';
- $out = sys_get_temp_dir() . '/dwziptest' . md5(time());
-
- $zip = new Zip();
- $file = "$dir/test.zip";
-
- $zip->open($file);
- $zip->close();
- $zip->extract($out);
- }
-
- /**
- * Extract the prebuilt zip files with component stripping
- * @depends testExtZipIsInstalled
- */
- public function testCompStripExtract()
- {
- $dir = $this->getDir() . '/zip';
- $out = sys_get_temp_dir() . '/dwziptest' . md5(time());
-
- $zip = new Zip();
- $file = "$dir/test.zip";
-
- $zip->open($file);
- $zip->extract($out, 1);
-
- clearstatcache();
-
- $this->assertFileExists($out . '/testdata1.txt', "Extracted $file");
- $this->assertEquals(13, filesize($out . '/testdata1.txt'), "Extracted $file");
-
- $this->assertFileExists($out . '/foobar/testdata2.txt', "Extracted $file");
- $this->assertEquals(13, filesize($out . '/foobar/testdata2.txt'), "Extracted $file");
-
- self::RDelete($out);
- }
-
- /**
- * Extract the prebuilt zip files with prefix stripping
- * @depends testExtZipIsInstalled
- */
- public function testPrefixStripExtract()
- {
- $dir = $this->getDir() . '/zip';
- $out = sys_get_temp_dir() . '/dwziptest' . md5(time());
-
- $zip = new Zip();
- $file = "$dir/test.zip";
-
- $zip->open($file);
- $zip->extract($out, 'zip/foobar/');
-
- clearstatcache();
-
- $this->assertFileExists($out . '/zip/testdata1.txt', "Extracted $file");
- $this->assertEquals(13, filesize($out . '/zip/testdata1.txt'), "Extracted $file");
-
- $this->assertFileExists($out . '/testdata2.txt', "Extracted $file");
- $this->assertEquals(13, filesize($out . '/testdata2.txt'), "Extracted $file");
-
- self::RDelete($out);
- }
-
- /**
- * Extract the prebuilt zip files with include regex
- * @depends testExtZipIsInstalled
- */
- public function testIncludeExtract()
- {
- $dir = $this->getDir() . '/zip';
- $out = sys_get_temp_dir() . '/dwziptest' . md5(time());
-
- $zip = new Zip();
- $file = "$dir/test.zip";
-
- $zip->open($file);
- $zip->extract($out, '', '', '/\/foobar\//');
-
- clearstatcache();
-
- $this->assertFileNotExists($out . '/zip/testdata1.txt', "Extracted $file");
-
- $this->assertFileExists($out . '/zip/foobar/testdata2.txt', "Extracted $file");
- $this->assertEquals(13, filesize($out . '/zip/foobar/testdata2.txt'), "Extracted $file");
-
- self::RDelete($out);
- }
-
- /**
- * Extract the prebuilt zip files with exclude regex
- * @depends testExtZipIsInstalled
- */
- public function testExcludeExtract()
- {
- $dir = $this->getDir() . '/zip';
- $out = sys_get_temp_dir() . '/dwziptest' . md5(time());
-
- $zip = new Zip();
- $file = "$dir/test.zip";
-
- $zip->open($file);
- $zip->extract($out, '', '/\/foobar\//');
-
- clearstatcache();
-
- $this->assertFileExists($out . '/zip/testdata1.txt', "Extracted $file");
- $this->assertEquals(13, filesize($out . '/zip/testdata1.txt'), "Extracted $file");
-
- $this->assertFileNotExists($out . '/zip/foobar/testdata2.txt', "Extracted $file");
-
- self::RDelete($out);
- }
-
- /**
- * @depends testExtZipIsInstalled
- */
- public function testUmlautWinrar()
- {
- $out = vfsStream::url('home_root_path/dwtartest' . md5(time()));
-
- $zip = new Zip();
- $zip->open($this->getDir() . '/zip/issue14-winrar.zip');
- $zip->extract($out);
- $this->assertFileExists("$out/tüst.txt");
- }
-
- /**
- * @depends testExtZipIsInstalled
- */
- public function testUmlautWindows()
- {
- $out = vfsStream::url('home_root_path/dwtartest' . md5(time()));
-
- $zip = new Zip();
- $zip->open($this->getDir() . '/zip/issue14-windows.zip');
- $zip->extract($out);
- $this->assertFileExists("$out/täst.txt");
- }
-
- /**
- * Create an archive, extract it, and compare file properties
- */
- public function testFilePropertiesPreservation()
- {
- $input = glob($this->getDir() . '/../src/*');
- $archive = sys_get_temp_dir() . '/dwtartest' . md5(time()) . '.zip';
- $extract = sys_get_temp_dir() . '/dwtartest' . md5(time() + 1);
-
- // Create archive
- $zip = new Zip();
- $zip->create($archive);
- foreach ($input as $path) {
- $file = basename($path);
- $zip->addFile($path, $file);
- }
- $zip->close();
- $this->assertFileExists($archive);
-
- // Extract archive
- $zip = new Zip();
- $zip->open($archive);
- $zip->extract($extract);
- $zip->close();
-
- // Compare file properties
- foreach ($input as $originalPath) {
- $filename = basename($originalPath);
- $extractedPath = $extract . '/' . $filename;
-
- $this->assertFileExists($extractedPath, "Extracted file should exist: $filename");
-
- // Compare file sizes
- $originalSize = filesize($originalPath);
- $extractedSize = filesize($extractedPath);
- $this->assertEquals($originalSize, $extractedSize, "File size should match for: $filename");
-
- // Compare file contents
- $originalContent = file_get_contents($originalPath);
- $extractedContent = file_get_contents($extractedPath);
- $this->assertEquals($originalContent, $extractedContent, "File content should match for: $filename");
-
- // Compare modification times (allow small difference due to tar format limitations)
- $originalMtime = filemtime($originalPath);
- $extractedMtime = filemtime($extractedPath);
- $this->assertLessThanOrEqual(1, abs($originalMtime - $extractedMtime),
- "Modification time should be preserved (within 1 second) for: $filename");
-
- // Compare file permissions (only on Unix-like systems)
- if (DIRECTORY_SEPARATOR === '/') {
- $originalPerms = fileperms($originalPath) & 0777;
- $extractedPerms = fileperms($extractedPath) & 0777;
- $this->assertEquals($originalPerms, $extractedPerms,
- "File permissions should match for: $filename");
- }
- }
-
- self::RDelete($extract);
- unlink($archive);
- }
-
-
- /**
- * recursive rmdir()/unlink()
- *
- * @static
- * @param $target string
- */
- public static function RDelete($target)
- {
- if (!is_dir($target)) {
- unlink($target);
- } else {
- $dh = dir($target);
- while (false !== ($entry = $dh->read())) {
- if ($entry == '.' || $entry == '..') {
- continue;
- }
- self::RDelete("$target/$entry");
- }
- $dh->close();
- rmdir($target);
- }
- }
-}
diff --git a/tests/tar/block.txt b/tests/tar/block.txt
deleted file mode 100644
index 9b2f530..0000000
--- a/tests/tar/block.txt
+++ /dev/null
@@ -1 +0,0 @@
-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
\ No newline at end of file
diff --git a/tests/tar/foobar/testdata2.txt b/tests/tar/foobar/testdata2.txt
deleted file mode 100644
index a7db157..0000000
--- a/tests/tar/foobar/testdata2.txt
+++ /dev/null
@@ -1 +0,0 @@
-testcontent2
diff --git a/tests/tar/longpath-gnu.tgz b/tests/tar/longpath-gnu.tgz
deleted file mode 100644
index 6c937c8..0000000
Binary files a/tests/tar/longpath-gnu.tgz and /dev/null differ
diff --git a/tests/tar/longpath-ustar.tgz b/tests/tar/longpath-ustar.tgz
deleted file mode 100644
index 59efbff..0000000
Binary files a/tests/tar/longpath-ustar.tgz and /dev/null differ
diff --git a/tests/tar/tarbomb.tgz b/tests/tar/tarbomb.tgz
deleted file mode 100644
index 8418d40..0000000
Binary files a/tests/tar/tarbomb.tgz and /dev/null differ
diff --git a/tests/tar/test.tar b/tests/tar/test.tar
deleted file mode 100644
index 931866b..0000000
Binary files a/tests/tar/test.tar and /dev/null differ
diff --git a/tests/tar/test.tar.bz2 b/tests/tar/test.tar.bz2
deleted file mode 100644
index 5a73740..0000000
Binary files a/tests/tar/test.tar.bz2 and /dev/null differ
diff --git a/tests/tar/test.tar.guess b/tests/tar/test.tar.guess
deleted file mode 100644
index 931866b..0000000
Binary files a/tests/tar/test.tar.guess and /dev/null differ
diff --git a/tests/tar/test.tar.gz b/tests/tar/test.tar.gz
deleted file mode 100644
index b003196..0000000
Binary files a/tests/tar/test.tar.gz and /dev/null differ
diff --git a/tests/tar/test.tbz b/tests/tar/test.tbz
deleted file mode 100644
index 5a73740..0000000
Binary files a/tests/tar/test.tbz and /dev/null differ
diff --git a/tests/tar/test.tbz.guess b/tests/tar/test.tbz.guess
deleted file mode 100644
index 5a73740..0000000
Binary files a/tests/tar/test.tbz.guess and /dev/null differ
diff --git a/tests/tar/test.tgz b/tests/tar/test.tgz
deleted file mode 100644
index b003196..0000000
Binary files a/tests/tar/test.tgz and /dev/null differ
diff --git a/tests/tar/test.tgz.guess b/tests/tar/test.tgz.guess
deleted file mode 100644
index b003196..0000000
Binary files a/tests/tar/test.tgz.guess and /dev/null differ
diff --git a/tests/tar/testdata1.txt b/tests/tar/testdata1.txt
deleted file mode 100644
index ac65bb3..0000000
--- a/tests/tar/testdata1.txt
+++ /dev/null
@@ -1 +0,0 @@
-testcontent1
diff --git a/tests/tar/zero.txt b/tests/tar/zero.txt
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/zip/block.txt b/tests/zip/block.txt
deleted file mode 100644
index 9b2f530..0000000
--- a/tests/zip/block.txt
+++ /dev/null
@@ -1 +0,0 @@
-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
\ No newline at end of file
diff --git a/tests/zip/foobar/testdata2.txt b/tests/zip/foobar/testdata2.txt
deleted file mode 100644
index a7db157..0000000
--- a/tests/zip/foobar/testdata2.txt
+++ /dev/null
@@ -1 +0,0 @@
-testcontent2
diff --git a/tests/zip/issue14-windows.zip b/tests/zip/issue14-windows.zip
deleted file mode 100644
index b5e4f9a..0000000
Binary files a/tests/zip/issue14-windows.zip and /dev/null differ
diff --git a/tests/zip/issue14-winrar.zip b/tests/zip/issue14-winrar.zip
deleted file mode 100644
index ec7d2c1..0000000
Binary files a/tests/zip/issue14-winrar.zip and /dev/null differ
diff --git a/tests/zip/test.zip b/tests/zip/test.zip
deleted file mode 100644
index c241e51..0000000
Binary files a/tests/zip/test.zip and /dev/null differ
diff --git a/tests/zip/testdata1.txt b/tests/zip/testdata1.txt
deleted file mode 100644
index ac65bb3..0000000
--- a/tests/zip/testdata1.txt
+++ /dev/null
@@ -1 +0,0 @@
-testcontent1
diff --git a/tests/zip/zero.txt b/tests/zip/zero.txt
deleted file mode 100644
index e69de29..0000000
diff --git a/tree.html b/tree.html
new file mode 100644
index 0000000..8e78337
--- /dev/null
+++ b/tree.html
@@ -0,0 +1,153 @@
+
+
+
+
+
+ Tree
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Tree
+
+
Classes
+
+
+
+
+
Exceptions
+
+
+
+ Exception
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+