1 /* 2 * ContentDigestManager.js 3 * 4 * Sweet Home 3D, Copyright (c) 2024 Space Mushrooms <info@sweethome3d.com> 5 * 6 * This program is free software; you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation; either version 2 of the License, or 9 * (at your option) any later version. 10 * 11 * This program is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with this program; if not, write to the Free Software 18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 */ 20 21 // Requires jszip.min.js 22 // URLContent.js 23 24 /** 25 * Manager able to store and compute content digest to compare content data faster. 26 * @constructor 27 * @private 28 * @author Emmanuel Puybaret 29 */ 30 function ContentDigestManager() { 31 this.contentDigestsCache = {}; 32 this.permanentContents = {}; 33 } 34 35 /** 36 * Singleton 37 * @private 38 */ 39 ModelManager.instance = null; 40 41 /** 42 * Returns an instance of this singleton. 43 * @return {ModelManager} 44 */ 45 ContentDigestManager.getInstance = function() { 46 if (ContentDigestManager.instance == null) { 47 ContentDigestManager.instance = new ContentDigestManager(); 48 } 49 return ContentDigestManager.instance; 50 } 51 52 /** 53 * Returns <code>true</code> if the contents in parameter contains the same data, 54 * comparing their digest. If the digest of the contents was not 55 * {@linkplain #setContentDigest(Content, byte[]) set} directly or in cache, 56 * it will return <code>false</code>. 57 * @param {URLContent} content1 58 * @param {URLContent} content2 59 * @return {boolean} 60 */ 61 ContentDigestManager.prototype.equals = function(content1, content2) { 62 var content1Digest = this.contentDigestsCache [content1.getURL()]; 63 if (content1Digest == null) { 64 return false; 65 } else { 66 return content1Digest === this.contentDigestsCache [content2.getURL()]; 67 } 68 } 69 70 /** 71 * Sets the SHA-1 digest of the given <code>content</code>. 72 * It should be used for permanent contents like the ones coming for preferences. 73 * @param {URLContent} content 74 * @param {string} digest 75 */ 76 ContentDigestManager.prototype.setContentDigest = function(content, digest) { 77 this.contentDigestsCache [content.getURL()] = digest; 78 this.permanentContents [digest] = content; 79 } 80 81 /** 82 * Returns the permanent content which matches the given <code>content</code> 83 * or <code>null</code> if doesn't exist. 84 * @param {URLContent} content a content with its digest already computed 85 * @return {URLContent} 86 */ 87 ContentDigestManager.prototype.getPermanentContentDigest = function(content) { 88 var contentDigest = this.contentDigestsCache [content.getURL()]; 89 if (contentDigest !== undefined) { 90 var permanentContent = this.permanentContents [contentDigest]; 91 if (permanentContent !== undefined) { 92 return permanentContent; 93 } 94 } 95 96 return null; 97 } 98 99 /** 100 * Returns asynchronously the SHA-1 digest of the given content. 101 * @param {URLContent} content 102 * @param {digestReady: function, digestError: function} digestObserver 103 * @ignore 104 */ 105 ContentDigestManager.prototype.getContentDigest = function(content, digestObserver) { 106 var contentDigest = this.contentDigestsCache [content.getURL()]; 107 if (contentDigest === undefined) { 108 if (content.isJAREntry()) { 109 this.getZipContentDigest(content, digestObserver); 110 } else if (content instanceof LocalURLContent) { 111 var manager = this; 112 content.getBlob({ 113 blobReady: function(blob) { 114 if (blob.type === "application/zip") { 115 manager.getZipContentDigest(content, digestObserver); 116 } else { 117 manager.getURLContentDigest(content, digestObserver); 118 } 119 }, 120 blobError: function(status, error) { 121 digestObserver.digestError(status, error); 122 } 123 }); 124 } else { 125 this.getURLContentDigest(content, digestObserver); 126 } 127 } else { 128 digestObserver.digestReady(content, contentDigest); 129 } 130 } 131 132 /** 133 * Returns asynchronously the SHA-1 digest of the given content. 134 * @param {URLContent} content content containing zipped data 135 * @param {digestReady: function, digestError: function} digestObserver 136 * @private 137 */ 138 ContentDigestManager.prototype.getZipContentDigest = function(content, digestObserver) { 139 var manager = this; 140 ZIPTools.getZIP(content.isJAREntry() ? content.getJAREntryURL() : content.getURL(), false, { 141 zipReady : function(zip) { 142 try { 143 var entryName = content.isJAREntry() ? content.getJAREntryName() : ""; 144 var slashIndex = content instanceof HomeURLContent 145 ? entryName.indexOf('/') 146 : -1; 147 var entryDirectory = entryName.substring(0, slashIndex + 1); 148 var contentData = new Uint8Array(0); 149 var entries = slashIndex > 0 || !(content instanceof HomeURLContent) 150 ? zip.file(new RegExp("^" + entryDirectory + ".*")).sort(function(entry1, entry2) { return entry1.name === entry2.name ? 0 : (entry1.name < entry2.name ? 1 : -1); }) // Reverse order 151 : [zip.file(entryName)]; 152 153 for (var i = entries.length - 1; i >= 0 ; i--) { 154 var zipEntry = entries [i]; 155 if (zipEntry.name !== entryDirectory 156 && manager.isSignificant(zipEntry.name)) { 157 // Append entry data to contentData 158 var entryData = zipEntry.asUint8Array(); 159 var data = new Uint8Array(contentData.length + entryData.length); 160 data.set(contentData); 161 data.set(entryData, contentData.length); 162 contentData = data; 163 } 164 } 165 166 manager.computeContentDigest(contentData, function(digest) { 167 manager.contentDigestsCache [content.getURL()] = digest; 168 digestObserver.digestReady(content, digest); 169 }); 170 } catch (ex) { 171 this.zipError(ex); 172 } 173 }, 174 zipError : function(error) { 175 digestObserver.digestError(error, error.message); 176 } 177 }); 178 } 179 180 /** 181 * Returns asynchronously the SHA-1 digest of the given content. 182 * @param {URLContent} content content containing no zipped data 183 * @param {digestReady: function, digestError: function} digestObserver 184 * @private 185 */ 186 ContentDigestManager.prototype.getURLContentDigest = function(content, digestObserver) { 187 var manager = this; 188 content.getStreamURL({ 189 urlReady: function(url) { 190 var request = new XMLHttpRequest(); 191 request.open("GET", url, true); 192 request.responseType = "arraybuffer"; 193 request.addEventListener("load", function() { 194 manager.computeContentDigest(request.response, function(digest) { 195 manager.contentDigestsCache [content.getURL()] = digest; 196 digestObserver.digestReady(content, digest); 197 }); 198 }); 199 request.send(); 200 }, 201 urlError: function(status, error) { 202 digestObserver.digestError(status, error); 203 } 204 }); 205 } 206 207 /** 208 * Computes the digest of the given data and calls <code>observer</code> when digest is ready. 209 * @param {Uint8Array} contentData 210 * @param {function} observer callback which will receive in parameter the SHA-1 digest of contentData in Base64 211 * @private 212 */ 213 ContentDigestManager.prototype.computeContentDigest = function(contentData, observer) { 214 var crypto = window.msCrypto !== undefined ? window.msCrypto : window.crypto; 215 var digest; 216 try { 217 digest = crypto.subtle.digest("SHA-1", contentData); 218 } catch (ex) { 219 // Use SHA-256 instead even if secured hash is not needed here 220 digest = crypto.subtle.digest("SHA-256", contentData); 221 } 222 if (digest.then !== undefined) { 223 digest.then(function(hash) { 224 observer(btoa(String.fromCharCode.apply(null, new Uint8Array(hash)))); 225 }); 226 } else { 227 // IE 11 digest.result is available without promise support but only after a call to setTimeout 228 setTimeout(function() { 229 observer(btoa(String.fromCharCode.apply(null, new Uint8Array(digest.result)))); 230 }); 231 } 232 } 233 234 /** 235 * Returns <code>true</code> if entry name is significant to distinguish 236 * the data of a content from an other one. 237 * @param {string} entryName 238 * @return {boolean} 239 * @private 240 */ 241 ContentDigestManager.prototype.isSignificant = function(entryName) { 242 // Ignore LICENSE.TXT files 243 var entryNameUpperCase = entryName.toUpperCase(); 244 return entryNameUpperCase !== "LICENSE.TXT" 245 && entryNameUpperCase.indexOf("/LICENSE.TXT", entryNameUpperCase.length - "/LICENSE.TXT".length) === -1; 246 } 247