Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Holy restructuring, batman! Watch out for falling folders.

  • Loading branch information...
commit 9fa3da535aeca157b674d8c4c15c1bec3daeafad 0 parents
andym authored
Showing with 4,181 additions and 0 deletions.
  1. BIN  Documentation.zip
  2. +26 −0 Info.plist
  3. +7 −0 License.txt
  4. +7 −0 Makefile
  5. +13 −0 NSApplication+AppCopies.h
  6. +27 −0 NSApplication+AppCopies.m
  7. +11 −0 NSFileManager+Authentication.h
  8. +109 −0 NSFileManager+Authentication.m
  9. +15 −0 NSFileManager+Verification.h
  10. +153 −0 NSFileManager+Verification.m
  11. +61 −0 NSString+extras.h
  12. +135 −0 NSString+extras.m
  13. +98 −0 RSS.h
  14. +690 −0 RSS.m
  15. +129 −0 Release Notes.rtf
  16. +27 −0 SUAppcast.h
  17. +80 −0 SUAppcast.m
  18. +62 −0 SUAppcastItem.h
  19. +184 −0 SUAppcastItem.m
  20. +21 −0 SUAutomaticUpdateAlert.h
  21. +61 −0 SUAutomaticUpdateAlert.m
  22. +20 −0 SUConstants.h
  23. +20 −0 SUConstants.m
  24. +26 −0 SUStatusChecker.h
  25. +78 −0 SUStatusChecker.m
  26. +33 −0 SUStatusController.h
  27. +119 −0 SUStatusController.m
  28. +25 −0 SUUnarchiver.h
  29. +144 −0 SUUnarchiver.m
  30. +40 −0 SUUpdateAlert.h
  31. +185 −0 SUUpdateAlert.m
  32. +54 −0 SUUpdater+Authentication.m
  33. +131 −0 SUUpdater+DSA.m
  34. +57 −0 SUUpdater.h
  35. +603 −0 SUUpdater.m
  36. +21 −0 SUUtilities.h
  37. +207 −0 SUUtilities.m
  38. +22 −0 Sparkle.h
  39. BIN  Sparkle.icns
  40. +8 −0 Sparkle_Prefix.pch
  41. +381 −0 md5.c
  42. +91 −0 md5.h
BIN  Documentation.zip
Binary file not shown
26 Info.plist
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>English</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundleIconFile</key>
+ <string></string>
+ <key>CFBundleIdentifier</key>
+ <string>org.andymatuschak.Sparkle</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundlePackageType</key>
+ <string>FMWK</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.1</string>
+ <key>NSPrincipalClass</key>
+ <string></string>
+</dict>
+</plist>
7 License.txt
@@ -0,0 +1,7 @@
+Copyright (c) 2006 Andy Matuschak
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
7 Makefile
@@ -0,0 +1,7 @@
+.PHONY: all localizable-strings
+
+localizable-strings:
+ rm English.lproj/Sparkle.strings || TRUE
+ genstrings -o English.lproj -s SULocalizedString *.m *.h
+ mv English.lproj/Localizable.strings English.lproj/Sparkle.strings
+
13 NSApplication+AppCopies.h
@@ -0,0 +1,13 @@
+//
+// NSApplication+AppCopies.h
+// Sparkle
+//
+// Created by Andy Matuschak on 3/16/06.
+// Copyright 2006 Andy Matuschak. All rights reserved.
+//
+
+#import <Cocoa/Cocoa.h>
+
+@interface NSApplication (SUAppCopies)
+- (int)copiesRunning;
+@end
27 NSApplication+AppCopies.m
@@ -0,0 +1,27 @@
+//
+// NSApplication+AppCopies.m
+// Sparkle
+//
+// Created by Andy Matuschak on 3/16/06.
+// Copyright 2006 Andy Matuschak. All rights reserved.
+//
+
+#import "NSApplication+AppCopies.h"
+#import "SUUtilities.h"
+
+@implementation NSApplication (SUAppCopies)
+
+- (int)copiesRunning
+{
+ id appEnumerator = [[[NSWorkspace sharedWorkspace] launchedApplications] objectEnumerator], currentApp;
+ int count = 0;
+ while ((currentApp = [appEnumerator nextObject]))
+ {
+ // Potential gotcha: the new version of your app better have the same NSApplicationName.
+ if([[currentApp objectForKey:@"NSApplicationName"] isEqualToString:SUHostAppName()])
+ count++;
+ }
+ return count;
+}
+
+@end
11 NSFileManager+Authentication.h
@@ -0,0 +1,11 @@
+//
+// NSFileManager+Authentication.m
+// Sparkle
+//
+// Created by Andy Matuschak on 3/9/06.
+// Copyright 2006 Andy Matuschak. All rights reserved.
+//
+
+@interface NSFileManager (SUAuthenticationAdditions)
+- (BOOL)movePathWithAuthentication:(NSString *)src toPath:(NSString *)dst;
+@end
109 NSFileManager+Authentication.m
@@ -0,0 +1,109 @@
+//
+// NSFileManager+Authentication.m
+// Sparkle
+//
+// Created by Andy Matuschak on 3/9/06.
+// Copyright 2006 Andy Matuschak. All rights reserved.
+//
+
+// This code based on generous contribution from Allan Odgaard. Thanks, Allan!
+
+#import "sys/stat.h"
+#import <Security/Security.h>
+
+#import <unistd.h>
+#import <sys/stat.h>
+#import <dirent.h>
+
+@implementation NSFileManager (SUAuthenticationAdditions)
+
+- (BOOL)currentUserOwnsPath:(NSString *)oPath
+{
+ char *path = (char *)[oPath fileSystemRepresentation];
+ unsigned int uid = getuid();
+ bool res = false;
+ struct stat sb;
+ if(stat(path, &sb) == 0)
+ {
+ if(sb.st_uid == uid)
+ {
+ res = true;
+ if(sb.st_mode & S_IFDIR)
+ {
+ DIR* dir = opendir(path);
+ struct dirent* entry = NULL;
+ while(res && (entry = readdir(dir)))
+ {
+ if(strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
+ continue;
+
+ char descend[strlen(path) + 1 + entry->d_namlen + 1];
+ strcpy(descend, path);
+ strcat(descend, "/");
+ strcat(descend, entry->d_name);
+ res = [self currentUserOwnsPath:[NSString stringWithUTF8String:descend]];
+ }
+ closedir(dir);
+ }
+ }
+ }
+ return res;
+}
+
+- (BOOL)_movePathWithForcedAuthentication:(NSString *)src toPath:(NSString *)dst
+{
+ NSString *tmp = [[[dst stringByDeletingPathExtension] stringByAppendingString:@".old"] stringByAppendingPathExtension:[dst pathExtension]];
+ BOOL res = NO;
+ struct stat sb;
+ if((stat([src UTF8String], &sb) != 0) || (stat([tmp UTF8String], &sb) == 0) || stat([dst UTF8String], &sb) != 0)
+ return false;
+
+ char* buf = NULL;
+ asprintf(&buf,
+ "mv -f \"$DST_PATH\" \"$TMP_PATH\" && "
+ "mv -f \"$SRC_PATH\" \"$DST_PATH\" && "
+ "rm -rf \"$TMP_PATH\" && "
+ "chown -R %d:%d \"$DST_PATH\"",
+ sb.st_uid, sb.st_gid);
+
+ if(!buf)
+ return false;
+
+ AuthorizationRef auth;
+ if(AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &auth) == errAuthorizationSuccess)
+ {
+ setenv("SRC_PATH", [src UTF8String], 1);
+ setenv("DST_PATH", [dst UTF8String], 1);
+ setenv("TMP_PATH", [tmp UTF8String], 1);
+ sig_t oldSigChildHandler = signal(SIGCHLD, SIG_DFL);
+ char const* arguments[] = { "-c", buf, NULL };
+ if(AuthorizationExecuteWithPrivileges(auth, "/bin/sh", kAuthorizationFlagDefaults, (char**)arguments, NULL) == errAuthorizationSuccess)
+ {
+ int status;
+ int pid = wait(&status);
+ if(pid != -1 && WIFEXITED(status) && WEXITSTATUS(status) == 0)
+ res = YES;
+ }
+ signal(SIGCHLD, oldSigChildHandler);
+ }
+ AuthorizationFree(auth, 0);
+ free(buf);
+ return res;
+}
+
+- (BOOL)movePathWithAuthentication:(NSString *)src toPath:(NSString *)dst
+{
+ if ([[NSFileManager defaultManager] isWritableFileAtPath:dst] && [[NSFileManager defaultManager] isWritableFileAtPath:[dst stringByDeletingLastPathComponent]])
+ {
+ int tag = 0;
+ BOOL result = [[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation source:[dst stringByDeletingLastPathComponent] destination:@"" files:[NSArray arrayWithObject:[dst lastPathComponent]] tag:&tag];
+ result &= [[NSFileManager defaultManager] movePath:src toPath:dst handler:NULL];
+ return result;
+ }
+ else
+ {
+ return [self _movePathWithForcedAuthentication:src toPath:dst];
+ }
+}
+
+@end
15 NSFileManager+Verification.h
@@ -0,0 +1,15 @@
+//
+// NSFileManager+Verification.h
+// Sparkle
+//
+// Created by Andy Matuschak on 3/16/06.
+// Copyright 2006 Andy Matuschak. All rights reserved.
+//
+
+#import <Cocoa/Cocoa.h>
+
+// For the paranoid folks!
+@interface NSFileManager (SUVerification)
+- (BOOL)validatePath:(NSString *)path withMD5Hash:(NSString *)hash;
+- (BOOL)validatePath:(NSString *)path withEncodedDSASignature:(NSString *)encodedSignature;
+@end
153 NSFileManager+Verification.m
@@ -0,0 +1,153 @@
+//
+// NSFileManager+Verification.m
+// Sparkle
+//
+// Created by Andy Matuschak on 3/16/06.
+// Copyright 2006 Andy Matuschak. All rights reserved.
+//
+
+// DSA stuff adapted from code provided by Allan Odgaard. Thanks, Allan!
+
+#import "NSFileManager+Verification.h"
+#import "SUUtilities.h"
+#import "md5.h"
+
+#import <stdio.h>
+#import <openssl/evp.h>
+#import <openssl/bio.h>
+#import <openssl/pem.h>
+#import <openssl/rsa.h>
+#import <openssl/sha.h>
+
+int b64decode(unsigned char* str)
+{
+ unsigned char *cur, *start;
+ int d, dlast, phase;
+ unsigned char c;
+ static int table[256] = {
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* 00-0F */
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* 10-1F */
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63, /* 20-2F */
+ 52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1, /* 30-3F */
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14, /* 40-4F */
+ 15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1, /* 50-5F */
+ -1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40, /* 60-6F */
+ 41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1, /* 70-7F */
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* 80-8F */
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* 90-9F */
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* A0-AF */
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* B0-BF */
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* C0-CF */
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* D0-DF */
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* E0-EF */
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 /* F0-FF */
+ };
+
+ d = dlast = phase = 0;
+ start = str;
+ for (cur = str; *cur != '\0'; ++cur )
+ {
+ if(*cur == '\n' || *cur == '\r'){phase = dlast = 0; continue;}
+ d = table[(int)*cur];
+ if(d != -1)
+ {
+ switch(phase)
+ {
+ case 0:
+ ++phase;
+ break;
+ case 1:
+ c = ((dlast << 2) | ((d & 0x30) >> 4));
+ *str++ = c;
+ ++phase;
+ break;
+ case 2:
+ c = (((dlast & 0xf) << 4) | ((d & 0x3c) >> 2));
+ *str++ = c;
+ ++phase;
+ break;
+ case 3:
+ c = (((dlast & 0x03 ) << 6) | d);
+ *str++ = c;
+ phase = 0;
+ break;
+ }
+ dlast = d;
+ }
+ }
+ *str = '\0';
+ return str - start;
+}
+
+EVP_PKEY* load_dsa_key(char *key)
+{
+ EVP_PKEY* pkey = NULL;
+ BIO *bio;
+ if((bio = BIO_new_mem_buf(key, strlen(key))))
+ {
+ DSA* dsa_key = 0;
+ if(PEM_read_bio_DSA_PUBKEY(bio, &dsa_key, NULL, NULL))
+ {
+ if((pkey = EVP_PKEY_new()))
+ {
+ if(EVP_PKEY_assign_DSA(pkey, dsa_key) != 1)
+ {
+ DSA_free(dsa_key);
+ EVP_PKEY_free(pkey);
+ pkey = NULL;
+ }
+ }
+ }
+ BIO_free(bio);
+ }
+ return pkey;
+}
+
+@implementation NSFileManager (SUVerification)
+
+- (BOOL)validatePath:(NSString *)path withMD5Hash:(NSString *)hash
+{
+ NSData *data = [NSData dataWithContentsOfFile:path];
+ if (!data) { return NO; }
+
+ md5_state_t md5_state;
+ md5_init(&md5_state);
+ md5_append(&md5_state, [data bytes], [data length]);
+ unsigned char digest[16];
+ md5_finish(&md5_state, digest);
+
+ int di;
+ char hexDigest[32];
+ for (di = 0; di < 16; di++)
+ sprintf(hexDigest + di*2, "%02x", digest[di]);
+
+ return [hash isEqualToString:[NSString stringWithCString:hexDigest]];
+}
+
+- (BOOL)validatePath:(NSString *)path withEncodedDSASignature:(NSString *)encodedSignature
+{
+ EVP_PKEY* pkey;
+ if(!encodedSignature || !SUInfoValueForKey(SUPublicDSAKeyKey) || !(pkey = load_dsa_key((char *)[SUInfoValueForKey(SUPublicDSAKeyKey) UTF8String])))
+ return NO;
+
+ // Now, the signature is in base64; we have to decode it into a binary stream.
+ unsigned char *signature = (unsigned char *)[encodedSignature UTF8String];
+ long length = b64decode(signature);
+
+ NSData *pathData = [NSData dataWithContentsOfFile:path];
+ if (!pathData) { return NO; }
+ unsigned char md[SHA_DIGEST_LENGTH];
+ SHA1([pathData bytes], [pathData length], md);
+
+ BOOL res = false;
+ EVP_MD_CTX ctx;
+ if(EVP_VerifyInit(&ctx, EVP_dss1()) == 1)
+ {
+ EVP_VerifyUpdate(&ctx, md, SHA_DIGEST_LENGTH);
+ res = EVP_VerifyFinal(&ctx, signature, length, pkey) == 1;
+ }
+ EVP_PKEY_free(pkey);
+ return res;
+}
+
+@end
61 NSString+extras.h
@@ -0,0 +1,61 @@
+/*
+
+BSD License
+
+Copyright (c) 2002, Brent Simmons
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+* Neither the name of ranchero.com or Brent Simmons nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+*/
+
+
+/*
+ NSString+extras.h
+ NetNewsWire
+
+ Created by Brent Simmons on Fri Jun 14 2002.
+ Copyright (c) 2002 Brent Simmons. All rights reserved.
+*/
+
+
+#import <Foundation/Foundation.h>
+#import <CoreFoundation/CoreFoundation.h>
+
+
+@interface NSString (extras)
+
+- (NSString *)stringWithSubstitute:(NSString *)subs forCharactersFromSet:(NSCharacterSet *)set;
+
+- (NSString *) trimWhiteSpace;
+
+- (NSString *) stripHTML;
+
+- (NSString *) ellipsizeAfterNWords: (int) n;
+
++ (BOOL) stringIsEmpty: (NSString *) s;
+
+
+@end
135 NSString+extras.m
@@ -0,0 +1,135 @@
+/*
+
+BSD License
+
+Copyright (c) 2002, Brent Simmons
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+* Neither the name of ranchero.com or Brent Simmons nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+*/
+
+
+/*
+ NSString+extras.m
+ NetNewsWire
+
+ Created by Brent Simmons on Fri Jun 14 2002.
+ Copyright (c) 2002 Brent Simmons. All rights reserved.
+*/
+
+#import "NSString+extras.h"
+
+
+@implementation NSString (extras)
+
+- (NSString *)stringWithSubstitute:(NSString *)subs forCharactersFromSet:(NSCharacterSet *)set
+{
+ NSRange r = [self rangeOfCharacterFromSet:set];
+ if (r.location == NSNotFound) return self;
+ NSMutableString *newString = [self mutableCopy];
+ do
+ {
+ [newString replaceCharactersInRange:r withString:subs];
+ r = [newString rangeOfCharacterFromSet:set];
+ }
+ while (r.location != NSNotFound);
+ return [newString autorelease];
+}
+
+- (NSString *) trimWhiteSpace {
+
+ NSMutableString *s = [[self mutableCopy] autorelease];
+
+ CFStringTrimWhitespace ((CFMutableStringRef) s);
+
+ return (NSString *) [[s copy] autorelease];
+ } /*trimWhiteSpace*/
+
+
+- (NSString *) ellipsizeAfterNWords: (int) n {
+
+ NSArray *stringComponents = [self componentsSeparatedByString: @" "];
+ NSMutableArray *componentsCopy = [stringComponents mutableCopy];
+ int ix = n;
+ int len = [componentsCopy count];
+
+ if (len < n)
+ ix = len;
+
+ [componentsCopy removeObjectsInRange: NSMakeRange (ix, len - ix)];
+
+ return [componentsCopy componentsJoinedByString: @" "];
+ } /*ellipsizeAfterNWords*/
+
+
+- (NSString *) stripHTML {
+
+ int len = [self length];
+ NSMutableString *s = [NSMutableString stringWithCapacity: len];
+ int i = 0, level = 0;
+
+ for (i = 0; i < len; i++) {
+
+ NSString *ch = [self substringWithRange: NSMakeRange (i, 1)];
+
+ if ([ch isEqualTo: @"<"])
+ level++;
+
+ else if ([ch isEqualTo: @">"]) {
+
+ level--;
+
+ if (level == 0)
+ [s appendString: @" "];
+ } /*else if*/
+
+ else if (level == 0)
+ [s appendString: ch];
+ } /*for*/
+
+ return (NSString *) [[s copy] autorelease];
+ } /*stripHTML*/
+
+
++ (BOOL) stringIsEmpty: (NSString *) s {
+
+ NSString *copy;
+
+ if (s == nil)
+ return (YES);
+
+ if ([s isEqualTo: @""])
+ return (YES);
+
+ copy = [[s copy] autorelease];
+
+ if ([[copy trimWhiteSpace] isEqualTo: @""])
+ return (YES);
+
+ return (NO);
+ } /*stringIsEmpty*/
+
+@end
98 RSS.h
@@ -0,0 +1,98 @@
+/*
+
+BSD License
+
+Copyright (c) 2002, Brent Simmons
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+* Neither the name of ranchero.com or Brent Simmons nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+*/
+
+/*
+ RSS.h
+ A class for reading RSS feeds.
+
+ Created by Brent Simmons on Wed Apr 17 2002.
+ Copyright (c) 2002 Brent Simmons. All rights reserved.
+*/
+
+
+#import <Cocoa/Cocoa.h>
+#import <CoreFoundation/CoreFoundation.h>
+#import "NSString+extras.h"
+
+
+@interface RSS : NSObject {
+
+ NSDictionary *headerItems;
+ NSMutableArray *newsItems;
+ NSString *version;
+
+ BOOL flRdf;
+ BOOL normalize;
+ }
+
+
+/*Public*/
+
+- (RSS *) initWithTitle: (NSString *) title andDescription: (NSString *) description;
+
+- (RSS *) initWithData: (NSData *) rssData normalize: (BOOL) fl;
+
+- (RSS *) initWithURL: (NSURL *) url normalize: (BOOL) fl;
+- (RSS *) initWithURL: (NSURL *) url normalize: (BOOL) fl userAgent:(NSString *)userAgent;
+
+- (NSDictionary *) headerItems;
+
+- (NSMutableArray *) newsItems;
+
+- (NSString *) version;
+
+// AMM's extensions for Sparkle
+- (NSDictionary *)newestItem;
+
+
+/*Private*/
+
+- (void) createheaderdictionary: (CFXMLTreeRef) tree;
+
+- (void) createitemsarray: (CFXMLTreeRef) tree;
+
+- (void) setversionstring: (CFXMLTreeRef) tree;
+
+- (void) flattenimagechildren: (CFXMLTreeRef) tree into: (NSMutableDictionary *) dictionary;
+
+- (void) flattensourceattributes: (CFXMLNodeRef) node into: (NSMutableDictionary *) dictionary;
+
+- (CFXMLTreeRef) getchanneltree: (CFXMLTreeRef) tree;
+
+- (CFXMLTreeRef) getnamedtree: (CFXMLTreeRef) currentTree name: (NSString *) name;
+
+- (void) normalizeRSSItem: (NSMutableDictionary *) rssItem;
+
+- (NSString *) getelementvalue: (CFXMLTreeRef) tree;
+
+@end
690 RSS.m
@@ -0,0 +1,690 @@
+/*
+
+BSD License
+
+Copyright (c) 2002, Brent Simmons
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+* Neither the name of ranchero.com or Brent Simmons nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+*/
+
+/*
+ RSS.m
+ A class for reading RSS feeds.
+
+ Created by Brent Simmons on Wed Apr 17 2002.
+ Copyright (c) 2002 Brent Simmons. All rights reserved.
+*/
+
+
+#import "RSS.h"
+
+// This comparator function is used to sort the RSS items by their published date.
+int compareNewsItems(id item1, id item2, void *context)
+{
+ // We compare item2 with item1 instead of the other way 'round because we want descending, not ascending. Bit of a hack.
+ return [(NSDate *)[NSDate dateWithNaturalLanguageString:[item2 objectForKey:@"pubDate"]] compare:(NSDate *)[NSDate dateWithNaturalLanguageString:[item1 objectForKey:@"pubDate"]]];
+}
+
+@implementation RSS
+
+
+#define titleKey @"title"
+#define linkKey @"link"
+#define descriptionKey @"description"
+
+
+/*Public interface*/
+
+- (NSDictionary *)newestItem
+{
+ // The news items are already sorted by published date, descending.
+ return [[self newsItems] objectAtIndex:0];
+}
+
+- (RSS *) initWithTitle: (NSString *) title andDescription: (NSString *) description {
+
+ /*
+ Create an empty feed. Useful for synthetic feeds.
+ */
+
+ NSMutableDictionary *header;
+
+ flRdf = NO;
+
+ header = [NSMutableDictionary dictionaryWithCapacity: 2];
+
+ [header setObject: title forKey: titleKey];
+
+ [header setObject: description forKey: descriptionKey];
+
+ headerItems = (NSDictionary *) [header copy];
+
+ newsItems = [[NSMutableArray alloc] initWithCapacity: 0];
+
+ version = [[NSString alloc] initWithString: @"synthetic"];
+
+ return (self);
+ } /*initWithTitle*/
+
+
+- (RSS *) initWithData: (NSData *) rssData normalize: (BOOL) fl {
+
+ CFXMLTreeRef tree;
+
+ flRdf = NO;
+
+ normalize = fl;
+
+ NS_DURING
+
+ tree = CFXMLTreeCreateFromData (kCFAllocatorDefault, (CFDataRef) rssData,
+ NULL, kCFXMLParserSkipWhitespace, kCFXMLNodeCurrentVersion);
+
+ NS_HANDLER
+
+ tree = nil;
+
+ NS_ENDHANDLER
+
+ if (tree == nil) {
+
+ /*If there was a problem parsing the RSS file,
+ raise an exception.*/
+
+ [self release];
+ return nil;
+ } /*if*/
+
+ [self createheaderdictionary: tree];
+
+ [self createitemsarray: tree];
+
+ [self setversionstring: tree];
+
+ CFRelease (tree);
+
+ return (self);
+ } /*initWithData*/
+
+
+- (RSS *) initWithURL: (NSURL *) url normalize: (BOOL) fl
+{
+ return [self initWithURL: url normalize: fl userAgent: nil];
+}
+
+
+
+- (RSS *) initWithURL: (NSURL *) url normalize: (BOOL) fl userAgent: (NSString*)userAgent
+{
+ NSData *rssData;
+
+ NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL: url cachePolicy: NSURLRequestReloadIgnoringCacheData
+ timeoutInterval: 30.0];
+ if (userAgent)
+ [request setValue: userAgent forHTTPHeaderField: @"User-Agent"];
+
+ NSURLResponse *response=0;
+ NSError *error=0;
+
+ rssData = [NSURLConnection sendSynchronousRequest: request returningResponse: &response error: &error];
+
+ if (rssData == nil)
+ {
+ NSException *exception = [NSException exceptionWithName: @"RSSDownloadFailed"
+ reason: [error localizedFailureReason] userInfo: [error userInfo] ];
+ [exception raise];
+ }
+
+ return [self initWithData: rssData normalize: fl];
+} /*initWithUrl*/
+
+
+- (NSDictionary *) headerItems {
+
+ return (headerItems);
+ } /*headerItems*/
+
+
+- (NSMutableArray *) newsItems {
+
+ return (newsItems);
+ } /*newsItems*/
+
+
+- (NSString *) version {
+
+ return (version);
+ } /*version*/
+
+
+- (void) dealloc {
+
+ [headerItems release];
+
+ [newsItems release];
+
+ [version release];
+ [super dealloc];
+ } /*dealloc*/
+
+
+
+/*Private methods. Don't call these: they may change.*/
+
+
+- (void) createheaderdictionary: (CFXMLTreeRef) tree {
+
+ CFXMLTreeRef channelTree, childTree;
+ CFXMLNodeRef childNode;
+ int childCount, i;
+ NSString *childName;
+ NSMutableDictionary *headerItemsMutable;
+
+ channelTree = [self getchanneltree: tree];
+
+ if (channelTree == nil) {
+
+ NSException *exception = [NSException exceptionWithName: @"RSSCreateHeaderDictionaryFailed"
+ reason: @"Couldn't find the channel tree." userInfo: nil];
+
+ [exception raise];
+ } /*if*/
+
+ childCount = CFTreeGetChildCount (channelTree);
+
+ headerItemsMutable = [NSMutableDictionary dictionaryWithCapacity: childCount];
+
+ for (i = 0; i < childCount; i++) {
+
+ childTree = CFTreeGetChildAtIndex (channelTree, i);
+
+ childNode = CFXMLTreeGetNode (childTree);
+
+ childName = (NSString *) CFXMLNodeGetString (childNode);
+
+ if ([childName hasPrefix: @"rss:"])
+ childName = [childName substringFromIndex: 4];
+
+ if ([childName isEqualToString: @"item"])
+ break;
+
+ if ([childName isEqualTo: @"image"])
+ [self flattenimagechildren: childTree into: headerItemsMutable];
+
+ [headerItemsMutable setObject: [self getelementvalue: childTree] forKey: childName];
+ } /*for*/
+
+ headerItems = [headerItemsMutable copy];
+ } /*initheaderdictionary*/
+
+
+- (void) createitemsarray: (CFXMLTreeRef) tree {
+
+ CFXMLTreeRef channelTree, childTree, itemTree;
+ CFXMLNodeRef childNode, itemNode;
+ NSString *childName;
+ NSString *itemName, *itemValue;
+ int childCount, itemChildCount, i, j;
+ NSMutableDictionary *itemDictionaryMutable;
+ NSMutableArray *itemsArrayMutable;
+
+ if (flRdf)
+ channelTree = [self getnamedtree: tree name: @"rdf:RDF"];
+ else
+ channelTree = [self getchanneltree: tree];
+
+ if (channelTree == nil) {
+
+ NSException *exception = [NSException exceptionWithName: @"RSSCreateItemsArrayFailed"
+ reason: @"Couldn't find the news items." userInfo: nil];
+
+ [exception raise];
+ } /*if*/
+
+ childCount = CFTreeGetChildCount (channelTree);
+
+ itemsArrayMutable = [NSMutableArray arrayWithCapacity: childCount];
+
+ for (i = 0; i < childCount; i++) {
+
+ childTree = CFTreeGetChildAtIndex (channelTree, i);
+
+ childNode = CFXMLTreeGetNode (childTree);
+
+ childName = (NSString *) CFXMLNodeGetString (childNode);
+
+ if ([childName hasPrefix: @"rss:"])
+ childName = [childName substringFromIndex: 4];
+
+ if (![childName isEqualToString: @"item"])
+ continue;
+
+ itemChildCount = CFTreeGetChildCount (childTree);
+
+ itemDictionaryMutable = [NSMutableDictionary dictionaryWithCapacity: itemChildCount];
+
+ for (j = 0; j < itemChildCount; j++) {
+
+ itemTree = CFTreeGetChildAtIndex (childTree, j);
+
+ itemNode = CFXMLTreeGetNode (itemTree);
+
+ itemName = (NSString *) CFXMLNodeGetString (itemNode);
+
+ if ([itemName hasPrefix: @"rss:"])
+ itemName = [itemName substringFromIndex: 4];
+
+ if ([itemName isEqualTo:@"enclosure"])
+ {
+ // Hack to add attributes to the dictionary in addition to children. (AMM)
+ const CFXMLElementInfo *websiteInfo = CFXMLNodeGetInfoPtr(itemNode);
+ NSMutableDictionary *enclosureDictionary = [NSMutableDictionary dictionary];
+ id keyEnumerator = [(NSDictionary *)websiteInfo->attributes keyEnumerator], current;
+ while ((current = [keyEnumerator nextObject]))
+ {
+ [enclosureDictionary setObject:[(NSDictionary *)websiteInfo->attributes objectForKey:current] forKey:current];
+ }
+ [itemDictionaryMutable setObject: enclosureDictionary forKey: itemName];
+ continue;
+ }
+
+ itemValue = [self getelementvalue: itemTree];
+
+ if ([itemName isEqualTo: @"source"])
+ [self flattensourceattributes: itemNode into: itemDictionaryMutable];
+
+ [itemDictionaryMutable setObject: itemValue forKey: itemName];
+ } /*for*/
+
+ if (normalize)
+ [self normalizeRSSItem: itemDictionaryMutable];
+
+ [itemsArrayMutable addObject: itemDictionaryMutable];
+ } /*for*/
+
+ // Sort the news items by published date, descending.
+ newsItems = [[itemsArrayMutable sortedArrayUsingFunction:compareNewsItems context:NULL] retain];
+ } /*createitemsarray*/
+
+
+- (void) setversionstring: (CFXMLTreeRef) tree {
+
+ CFXMLTreeRef rssTree;
+ const CFXMLElementInfo *elementInfo;
+ CFXMLNodeRef node;
+
+ if (flRdf) {
+
+ version = [[NSString alloc] initWithString: @"rdf"];
+
+ return;
+ } /*if*/
+
+ rssTree = [self getnamedtree: tree name: @"rss"];
+
+ node = CFXMLTreeGetNode (rssTree);
+
+ elementInfo = CFXMLNodeGetInfoPtr (node);
+
+ version = [[NSString alloc] initWithString: [(NSDictionary *) (*elementInfo).attributes objectForKey: @"version"]];
+ } /*setversionstring*/
+
+
+- (void) flattenimagechildren: (CFXMLTreeRef) tree into: (NSMutableDictionary *) dictionary {
+
+ int childCount = CFTreeGetChildCount (tree);
+ int i = 0;
+ CFXMLTreeRef childTree;
+ CFXMLNodeRef childNode;
+ NSString *childName, *childValue, *keyName;
+
+ if (childCount < 1)
+ return;
+
+ for (i = 0; i < childCount; i++) {
+
+ childTree = CFTreeGetChildAtIndex (tree, i);
+
+ childNode = CFXMLTreeGetNode (childTree);
+
+ childName = (NSString *) CFXMLNodeGetString (childNode);
+
+ if ([childName hasPrefix: @"rss:"])
+ childName = [childName substringFromIndex: 4];
+
+ childValue = [self getelementvalue: childTree];
+
+ keyName = [NSString stringWithFormat: @"image%@", childName];
+
+ [dictionary setObject: childValue forKey: keyName];
+ } /*for*/
+ } /*flattenimagechildren*/
+
+
+- (void) flattensourceattributes: (CFXMLNodeRef) node into: (NSMutableDictionary *) dictionary {
+
+ const CFXMLElementInfo *elementInfo;
+ NSString *sourceHomeUrl, *sourceRssUrl;
+
+ elementInfo = CFXMLNodeGetInfoPtr (node);
+
+ sourceHomeUrl = [(NSDictionary *) (*elementInfo).attributes objectForKey: @"homeUrl"];
+
+ sourceRssUrl = [(NSDictionary *) (*elementInfo).attributes objectForKey: @"url"];
+
+ if (sourceHomeUrl != nil)
+ [dictionary setObject: sourceHomeUrl forKey: @"sourceHomeUrl"];
+
+ if (sourceRssUrl != nil)
+ [dictionary setObject: sourceRssUrl forKey: @"sourceRssUrl"];
+ } /*flattensourceattributes*/
+
+
+- (CFXMLTreeRef) getchanneltree: (CFXMLTreeRef) tree {
+
+ CFXMLTreeRef rssTree, channelTree;
+
+ rssTree = [self getnamedtree: tree name: @"rss"];
+
+ if (rssTree == nil) { /*It might be "rdf:RDF" instead, a 1.0 or greater feed.*/
+
+ rssTree = [self getnamedtree: tree name: @"rdf:RDF"];
+
+ if (rssTree != nil)
+ flRdf = YES; /*This info will be needed later when creating the items array.*/
+ } /*if*/
+
+ if (rssTree == nil)
+ return (nil);
+
+ channelTree = [self getnamedtree: rssTree name: @"channel"];
+
+ if (channelTree == nil)
+ channelTree = [self getnamedtree: rssTree name: @"rss:channel"];
+
+ return (channelTree);
+ } /*getchanneltree*/
+
+
+- (CFXMLTreeRef) getnamedtree: (CFXMLTreeRef) currentTree name: (NSString *) name {
+
+ int childCount, i;
+ CFXMLNodeRef xmlNode;
+ CFXMLTreeRef xmlTreeNode;
+ NSString *itemName;
+
+ childCount = CFTreeGetChildCount (currentTree);
+
+ for (i = childCount - 1; i >= 0; i--) {
+
+ xmlTreeNode = CFTreeGetChildAtIndex (currentTree, i);
+
+ xmlNode = CFXMLTreeGetNode (xmlTreeNode);
+
+ itemName = (NSString *) CFXMLNodeGetString (xmlNode);
+
+ if ([itemName isEqualToString: name])
+ return (xmlTreeNode);
+ } /*for*/
+
+ return (nil);
+ } /*getnamedtree*/
+
+
+- (void) normalizeRSSItem: (NSMutableDictionary *) rssItem {
+
+ /*
+ Make sure item, link, and description are present and have
+ reasonable values. Description and link may be "".
+ Also trim white space, remove HTML when appropriate.
+ */
+
+ NSString *description, *link, *title;
+ BOOL nilDescription = NO;
+
+ /*Description*/
+
+ description = [rssItem objectForKey: descriptionKey];
+
+ if (description == nil) {
+
+ description = @"";
+
+ nilDescription = YES;
+ } /*if*/
+
+ description = [description trimWhiteSpace];
+
+ if ([description isEqualTo: @""])
+ nilDescription = YES;
+
+ [rssItem setObject: description forKey: descriptionKey];
+
+ /*Link*/
+
+ link = [rssItem objectForKey: linkKey];
+
+ if ([NSString stringIsEmpty: link]) {
+
+ /*Try to get a URL from the description.*/
+
+ if (!nilDescription) {
+
+ NSArray *stringComponents = [description componentsSeparatedByString: @"href=\""];
+
+ if ([stringComponents count] > 1) {
+
+ link = [stringComponents objectAtIndex: 1];
+
+ stringComponents = [link componentsSeparatedByString: @"\""];
+
+ link = [stringComponents objectAtIndex: 0];
+ } /*if*/
+ } /*if*/
+ } /*if*/
+
+ if (link == nil)
+ link = @"";
+
+ link = [link trimWhiteSpace];
+
+ [rssItem setObject: link forKey: linkKey];
+
+ /*Title*/
+
+ title = [rssItem objectForKey: titleKey];
+
+ if (title != nil) {
+
+ title = [title stripHTML];
+
+ title = [title trimWhiteSpace];
+ } /*if*/
+
+ if ([NSString stringIsEmpty: title]) {
+
+ /*Grab a title from the description.*/
+
+ if (!nilDescription) {
+
+ NSArray *stringComponents = [description componentsSeparatedByString: @">"];
+
+ if ([stringComponents count] > 1) {
+
+ title = [stringComponents objectAtIndex: 1];
+
+ stringComponents = [title componentsSeparatedByString: @"<"];
+
+ title = [stringComponents objectAtIndex: 0];
+
+ title = [title stripHTML];
+
+ title = [title trimWhiteSpace];
+ } /*if*/
+
+ if ([NSString stringIsEmpty: title]) { /*use first part of description*/
+
+ NSString *shortTitle = [[[description stripHTML] trimWhiteSpace] ellipsizeAfterNWords: 5];
+
+ shortTitle = [shortTitle trimWhiteSpace];
+
+ title = [NSString stringWithFormat: @"%@...", shortTitle];
+ } /*else*/
+ } /*if*/
+
+ title = [title stripHTML];
+
+ title = [title trimWhiteSpace];
+
+ if ([NSString stringIsEmpty: title])
+ title = @"Untitled";
+ } /*if*/
+
+ [rssItem setObject: title forKey: titleKey];
+
+ /*dangerousmeta case: super-long title with no description*/
+
+ if ((nilDescription) && ([title length] > 50)) {
+
+ NSString *shortTitle = [[[title stripHTML] trimWhiteSpace] ellipsizeAfterNWords: 7];
+
+ description = [[title copy] autorelease];
+
+ [rssItem setObject: description forKey: descriptionKey];
+
+ title = [NSString stringWithFormat: @"%@...", shortTitle];
+
+ [rssItem setObject: title forKey: titleKey];
+ } /*if*/
+
+ { /*deal with entities*/
+
+ const char *tempcstring;
+ NSAttributedString *s = nil;
+ NSString *convertedTitle = nil;
+ NSArray *stringComponents;
+
+ stringComponents = [title componentsSeparatedByString: @"&"];
+
+ if ([stringComponents count] > 1) {
+
+ stringComponents = [title componentsSeparatedByString: @";"];
+
+ if ([stringComponents count] > 1) {
+
+ int len;
+
+ tempcstring = [title UTF8String];
+
+ len = strlen (tempcstring);
+
+ if (len > 0) {
+
+ s = [[NSAttributedString alloc]
+ initWithHTML: [NSData dataWithBytes: tempcstring length: strlen (tempcstring)]
+ documentAttributes: (NSDictionary **) NULL];
+
+ convertedTitle = [s string];
+
+ [s autorelease];
+
+ convertedTitle = [convertedTitle stripHTML];
+
+ convertedTitle = [convertedTitle trimWhiteSpace];
+ } /*if*/
+
+ if ([NSString stringIsEmpty: convertedTitle])
+ convertedTitle = @"Untitled";
+
+ [rssItem setObject: convertedTitle forKey: @"convertedTitle"];
+ } /*if*/
+ } /*if*/
+ } /*deal with entities*/
+ } /*normalizeRSSItem*/
+
+
+- (NSString *) getelementvalue: (CFXMLTreeRef) tree {
+
+ CFXMLNodeRef node;
+ CFXMLTreeRef itemTree;
+ int childCount, ix;
+ NSMutableString *valueMutable;
+ NSString *value;
+ NSString *name;
+
+ childCount = CFTreeGetChildCount (tree);
+
+ valueMutable = [[NSMutableString alloc] init];
+
+ for (ix = 0; ix < childCount; ix++) {
+
+ itemTree = CFTreeGetChildAtIndex (tree, ix);
+
+ node = CFXMLTreeGetNode (itemTree);
+
+ name = (NSString *) CFXMLNodeGetString (node);
+
+ if (name != nil) {
+
+ if (CFXMLNodeGetTypeCode (node) == kCFXMLNodeTypeEntityReference) {
+
+ if ([name isEqualTo: @"lt"])
+ name = @"<";
+
+ else if ([name isEqualTo: @"gt"])
+ name = @">";
+
+ else if ([name isEqualTo: @"quot"])
+ name = @"\"";
+
+ else if ([name isEqualTo: @"amp"])
+ name = @"&";
+
+ else if ([name isEqualTo: @"rsquo"])
+ name = [NSString stringWithUTF8String:"\u2019"];
+
+ else if ([name isEqualTo: @"lsquo"])
+ name = [NSString stringWithUTF8String:"\u2018"];
+
+ else if ([name isEqualTo: @"apos"])
+ name = @"'";
+ else
+ name = [NSString stringWithFormat: @"&%@;", name];
+ } /*if*/
+
+ [valueMutable appendString: name];
+ } /*if*/
+ } /*for*/
+
+ value = [valueMutable copy];
+
+ [valueMutable autorelease];
+
+ return ([value autorelease]);
+ } /*getelementvalue*/
+
+@end
129 Release Notes.rtf
@@ -0,0 +1,129 @@
+{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410
+{\fonttbl\f0\fswiss\fcharset77 Helvetica-Bold;\f1\fswiss\fcharset77 Helvetica;\f2\fswiss\fcharset77 Helvetica-Oblique;
+}
+{\colortbl;\red255\green255\blue255;}
+{\*\listtable{\list\listtemplateid1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid0\'02\'05.;}{\levelnumbers\'01;}}{\listname ;}\listid1}
+{\list\listtemplateid2\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid0\'02\'05.;}{\levelnumbers\'01;}}{\listname ;}\listid2}
+{\list\listtemplateid3\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid0\'02\'05.;}{\levelnumbers\'01;}}{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{hyphen\}}{\leveltext\leveltemplateid1\'02\'05.;}{\levelnumbers\'01;}}{\listname ;}\listid3}
+{\list\listtemplateid4\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid0\'02\'05.;}{\levelnumbers\'01;}}{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{hyphen\}}{\leveltext\leveltemplateid1\'02\'05.;}{\levelnumbers\'01;}}{\listname ;}\listid4}
+{\list\listtemplateid5\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid0\'02\'05.;}{\levelnumbers\'01;}}{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{hyphen\}}{\leveltext\leveltemplateid1\'02\'05.;}{\levelnumbers\'01;}}{\listname ;}\listid5}
+{\list\listtemplateid6\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid0\'02\'05.;}{\levelnumbers\'01;}}{\listname ;}\listid6}
+{\list\listtemplateid7\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid0\'02\'05.;}{\levelnumbers\'01;}}{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{hyphen\}}{\leveltext\leveltemplateid1\'02\'05.;}{\levelnumbers\'01;}}{\listname ;}\listid7}
+{\list\listtemplateid8\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid0\'02\'05.;}{\levelnumbers\'01;}}{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{hyphen\}}{\leveltext\leveltemplateid1\'02\'05.;}{\levelnumbers\'01;}}{\listname ;}\listid8}
+{\list\listtemplateid9\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid0\'02\'05.;}{\levelnumbers\'01;}}{\listname ;}\listid9}}
+{\*\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}{\listoverride\listid2\listoverridecount0\ls2}{\listoverride\listid3\listoverridecount0\ls3}{\listoverride\listid4\listoverridecount0\ls4}{\listoverride\listid5\listoverridecount0\ls5}{\listoverride\listid6\listoverridecount0\ls6}{\listoverride\listid7\listoverridecount0\ls7}{\listoverride\listid8\listoverridecount0\ls8}{\listoverride\listid9\listoverridecount0\ls9}}
+\margl1440\margr1440\vieww9000\viewh8400\viewkind0
+\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural
+
+\f0\b\fs24 \cf0 Sparkle Updater
+\f1\b0 \
+Release Notes\
+\
+Version 1.1\
+\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\ql\qnatural\pardirnatural
+\ls1\ilvl0\cf0 {\listtext \'a5 }Optimized framework size: now only 1.4mb with all localizations and 384kb with only English (an English-only version is in the Extras folder).\
+{\listtext \'a5 }Added a new SUStatusChecker class for programmatically determining if a new version is available (see the docs); thanks, Evan Schoenberg!\
+{\listtext \'a5 }Added support for apps using SIGCHLD; thanks, Augie Fackler!\
+{\listtext \'a5 }Added a zh_CN update from JT Lee\
+{\listtext \'a5 }Added a Polish update from Piotr Chylinski\
+{\listtext \'a5 }Fixed DMG support for images with /Applications symlinks.\
+{\listtext \'a5 }Fixed a really stupid interval-checking bug that could cause repeated hits to the appcast.\
+{\listtext \'a5 }Fixed a bug where the check interval would be inconsistent if a value of 0 was stored in the user defaults.\
+\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural
+\cf0 \
+Version 1.0\
+\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\ql\qnatural\pardirnatural
+\ls2\ilvl0\cf0 {\listtext \'a5 }Additions:\
+\pard\tx940\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li1440\fi-1440\ql\qnatural\pardirnatural
+\ls3\ilvl1\cf0 {\listtext \uc0\u8259 }Added real version comparison courtesy Kevin Ballard: Sparkle now knows that 0.89 < 1.0a3 < 1.0.\
+{\listtext \uc0\u8259 }Added many localizations courtesy David Kocher's localization team.\
+\pard\tx940\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li1440\fi-1440\ql\qnatural\pardirnatural
+\ls4\ilvl1\cf0 {\listtext \uc0\u8259 }Added a much better installation mechanism courtesy Allan Odgaard.\
+{\listtext \uc0\u8259 }Added a user agent string to the RSS fetch request.\
+{\listtext \uc0\u8259 }Added support for CFBundleShortVersionString in addition to CFBundleVersion, and support for a sparkle:shortVersionString attribute on the enclosure.\
+{\listtext \uc0\u8259 }Added support for CFBundleDisplayName if available.\
+\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\ql\qnatural\pardirnatural
+\ls4\ilvl0\cf0 {\listtext \'a5 }Changes:\
+\pard\tx940\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li1440\fi-1440\ql\qnatural\pardirnatural
+\ls4\ilvl1\cf0 {\listtext \uc0\u8259 }Automatic updating is now allowed by default, but only if DSA signing is on.\
+{\listtext \uc0\u8259 }Pressing Escape or closing the update alert now reminds the user later.\
+{\listtext \uc0\u8259 }Now when there's a stored check interval, Sparkle doesn't check immediately on startup the first time the app is launched because the user hasn't consented to it yet.\
+{\listtext \uc0\u8259 }The update alert now remembers its size and floats.\
+\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\ql\qnatural\pardirnatural
+\ls4\ilvl0\cf0 {\listtext \'a5 }Bug Fixes:\
+\pard\tx940\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li1440\fi-1440\ql\qnatural\pardirnatural
+\ls4\ilvl1\cf0 {\listtext \uc0\u8259 }Fixed installation of DMGs with multiple files enclosed.\
+{\listtext \uc0\u8259 }Fixed a nasty memory leak.\
+{\listtext \uc0\u8259 }Fixed a bug wherein having no value for allowing automatic updates would display a checkbox for the updates but would not honor it.\
+{\listtext \uc0\u8259 }Fixed a bug in zip extraction that occurred in Panther.\
+{\listtext \uc0\u8259 }Fixed release notes caching.\
+{\listtext \uc0\u8259 }Fixed a bug wherein Sparkle refused to authenticate the installation if the user had cancelled authentication previously in that session.\
+{\listtext \uc0\u8259 }Fixed a weird bug that would cause a second help menu to appear on first launch.\
+\pard\tx940\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li1440\fi-1440\ql\qnatural\pardirnatural
+\ls5\ilvl1\cf0 {\listtext \uc0\u8259 }Fixed a bug that could occur when changing the scheduled check interval.\
+{\listtext \uc0\u8259 }Fixed a bug wherein the host app could crash if the user clicked Remind Me Later before the release notes finished loading.\
+{\listtext \uc0\u8259 }Fixed a bug wherein the behavior was undefined if the user manually initiated a check when an automatic one was already taking place.\
+{\listtext \uc0\u8259 }Fixed wrapping on the description field in the update alert.\
+\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural
+\cf0 \
+Version 1.0 (beta 3):\
+\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\ql\qnatural\pardirnatural
+\ls6\ilvl0\cf0 {\listtext \'a5 }Fixed a
+\f2\i nasty
+\f1\i0 crasher that occurred often when the user was not connected to the internet.\
+\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural
+\cf0 \
+Version 1.0 (beta 2):\
+\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\ql\qnatural\pardirnatural
+\ls7\ilvl0\cf0 {\listtext \'a5 }Major Improvements:\
+\pard\tx940\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li1440\fi-1440\ql\qnatural\pardirnatural
+\ls7\ilvl1\cf0 {\listtext \uc0\u8259 }Fully automatic updating! (see the Documentation: this is beta and off by default)\
+{\listtext \uc0\u8259 }Added support for DSA signatures (see the Documentation).\
+{\listtext \uc0\u8259 }Added support for MD5 sum verification.\
+{\listtext \uc0\u8259 }Added Security.framework-based authentication for installing to privileged directories.\
+{\listtext \uc0\u8259 }Huge refactoring of the codebase: there's now a Sparkle Xcode project, Sparkle is now a framework, and everything is modular / abstracted. And no more code-generated interface.\
+\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\ql\qnatural\pardirnatural
+\ls7\ilvl0\cf0 {\listtext \'a5 }Minor Improvements:\
+\pard\tx940\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li1440\fi-1440\ql\qnatural\pardirnatural
+\ls7\ilvl1\cf0 {\listtext \uc0\u8259 }A SUUpdaterWillRestartNotification is sent out before restarting now.\
+{\listtext \uc0\u8259 }Added key equivalents to alert panel buttons.\
+{\listtext \uc0\u8259 }Error handling is much prettier now: technical messages are not presented to the user anymore.\
+{\listtext \uc0\u8259 }There's now a test app for developers to see what Sparkle's like before using it.\
+{\listtext \uc0\u8259 }Wrote new, pretty, extremely thorough documentation.\
+\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\ql\qnatural\pardirnatural
+\ls7\ilvl0\cf0 {\listtext \'a5 }Bug Fixes:\
+\pard\tx940\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li1440\fi-1440\ql\qnatural\pardirnatural
+\ls7\ilvl1\cf0 {\listtext \uc0\u8259 }Relaunch behavior is much improved and shouldn't fail in huge apps anymore.\
+{\listtext \uc0\u8259 }Fixed a bug wherein a failing tar command could crash the host app.\
+{\listtext \uc0\u8259 }Sparkle now looks at InfoPlist.strings in addition to Info.plist.\
+{\listtext \uc0\u8259 }Fixed some stupid typos.\
+\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural
+\cf0 \
+Version 1.0 (beta 1):\
+\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\ql\qnatural\pardirnatural
+\ls8\ilvl0\cf0 {\listtext \'a5 }Major New Features:\
+\pard\tx940\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li1440\fi-1440\ql\qnatural\pardirnatural
+\ls8\ilvl1\cf0 {\listtext \uc0\u8259 }Sparkle now supports scheduled periodic updates\'d1read the Readme for information on how to use it.\
+{\listtext \uc0\u8259 }Sparkle now supports WebKit-based release notes (for CSS and full HTML), which it displays in the main update alert, not a separate panel. The Readme has much more information. Sparkle will, of course, fall back on NSTextView if the host app does not include WebKit.\
+\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\ql\qnatural\pardirnatural
+\ls8\ilvl0\cf0 {\listtext \'a5 }Minor New Features:\
+\pard\tx940\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li1440\fi-1440\ql\qnatural\pardirnatural
+\ls8\ilvl1\cf0 {\listtext \uc0\u8259 }Added support for .zip update archives.\
+{\listtext \uc0\u8259 }Added support for .dmg update archives.\
+{\listtext \uc0\u8259 }Implemented Remind Me Later to replace simple update cancellation.\
+{\listtext \uc0\u8259 }Implemented Skip This Version functionality.\
+{\listtext \uc0\u8259 }Added support for multiple feeds via the user defaults SUFeedURL key taking precedent over the one in Info.plist.\
+{\listtext \uc0\u8259 }Added support for Sparkle's custom XML namespace, which is optional but may prove useful. See the Readme for more information.\
+\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\ql\qnatural\pardirnatural
+\ls8\ilvl0\cf0 {\listtext \'a5 }Bug Fixes:\
+\pard\tx940\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li1440\fi-1440\ql\qnatural\pardirnatural
+\ls8\ilvl1\cf0 {\listtext \uc0\u8259 }Sparkle will no longer enter an inconsistent state if the user tries to update again while one is already in progress.\
+{\listtext \uc0\u8259 }Sparkle now uses CFBundleName to determine the application's name instead of the app's filename.\
+{\listtext \uc0\u8259 }Sparkle no longer crashes if the user cancels during extraction.\
+{\listtext \uc0\u8259 }Lots of code refactoring.\
+\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural
+\cf0 \
+Version 0.1:\
+\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\ql\qnatural\pardirnatural
+\ls9\ilvl0\cf0 {\listtext \'a5 }Initial Release\
+}
27 SUAppcast.h
@@ -0,0 +1,27 @@
+//
+// SUAppcast.h
+// Sparkle
+//
+// Created by Andy Matuschak on 3/12/06.
+// Copyright 2006 Andy Matuschak. All rights reserved.
+//
+
+#import <Cocoa/Cocoa.h>
+
+@class RSS, SUAppcastItem;
+@interface SUAppcast : NSObject {
+ NSArray *items;
+ id delegate;
+}
+
+- (void)fetchAppcastFromURL:(NSURL *)url;
+- (void)setDelegate:delegate;
+
+- (SUAppcastItem *)newestItem;
+- (NSArray *)items;
+
+@end
+
+@interface NSObject (SUAppcastDelegate)
+- appcastDidFinishLoading:(SUAppcast *)appcast;
+@end
80 SUAppcast.m
@@ -0,0 +1,80 @@
+//
+// SUAppcast.m
+// Sparkle
+//
+// Created by Andy Matuschak on 3/12/06.
+// Copyright 2006 Andy Matuschak. All rights reserved.
+//
+
+#import "SUAppcast.h"
+#import "SUAppcastItem.h"
+#import "SUUtilities.h"
+#import "RSS.h"
+
+@implementation SUAppcast
+
+- (void)fetchAppcastFromURL:(NSURL *)url
+{
+ [NSThread detachNewThreadSelector:@selector(_fetchAppcastFromURL:) toTarget:self withObject:url]; // let's not block the main thread
+}
+
+- (void)setDelegate:del
+{
+ delegate = del;
+}
+
+- (void)dealloc
+{
+ [items release];
+ [super dealloc];
+}
+
+- (SUAppcastItem *)newestItem
+{
+ return [items objectAtIndex:0]; // the RSS class takes care of sorting by published date, descending.
+}
+
+- (NSArray *)items
+{
+ return items;
+}
+
+- (void)_fetchAppcastFromURL:(NSURL *)url
+{
+ NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
+
+ RSS *feed;
+ @try
+ {
+ NSString *userAgent = [NSString stringWithFormat: @"%@/%@ (Mac OS X) Sparkle/1.0", SUHostAppName(), SUHostAppVersion()];
+
+ feed = [[RSS alloc] initWithURL:url normalize:YES userAgent:userAgent];
+ if (!feed)
+ [NSException raise:@"SUFeedException" format:@"Couldn't fetch feed from server."];
+
+ // Set up all the appcast items
+ NSMutableArray *tempItems = [NSMutableArray array];
+ id enumerator = [[feed newsItems] objectEnumerator], current;
+ while ((current = [enumerator nextObject]))
+ {
+ [tempItems addObject:[[[SUAppcastItem alloc] initWithDictionary:current] autorelease]];
+ }
+ items = [[NSArray arrayWithArray:tempItems] retain];
+ [feed release];
+
+ if ([delegate respondsToSelector:@selector(appcastDidFinishLoading:)])
+ [delegate performSelectorOnMainThread:@selector(appcastDidFinishLoading:) withObject:self waitUntilDone:NO];
+
+ }
+ @catch (NSException *e)
+ {
+ if ([delegate respondsToSelector:@selector(appcastDidFailToLoad:)])
+ [delegate performSelectorOnMainThread:@selector(appcastDidFailToLoad:) withObject:self waitUntilDone:NO];
+ }
+ @finally
+ {
+ [pool release];
+ }
+}
+
+@end
62 SUAppcastItem.h
@@ -0,0 +1,62 @@
+//
+// SUAppcastItem.h
+// Sparkle
+//
+// Created by Andy Matuschak on 3/12/06.
+// Copyright 2006 Andy Matuschak. All rights reserved.
+//
+
+#import <Cocoa/Cocoa.h>
+
+
+@interface SUAppcastItem : NSObject {
+ NSString *title;
+ NSDate *date;
+ NSString *description;
+
+ NSURL *releaseNotesURL;
+
+ NSString *DSASignature;
+ NSString *MD5Sum;
+
+ NSString *minimumSystemVersion;
+
+ NSURL *fileURL;
+ NSString *fileVersion;
+ NSString *versionString;
+}
+
+// Initializes with data from a dictionary provided by the RSS class.
+- initWithDictionary:(NSDictionary *)dict;
+
+- (NSString *)title;
+- (void)setTitle:(NSString *)aTitle;
+
+- (NSDate *)date;
+- (void)setDate:(NSDate *)aDate;
+
+- (NSString *)description;
+- (void)setDescription:(NSString *)aDescription;
+
+- (NSURL *)releaseNotesURL;
+- (void)setReleaseNotesURL:(NSURL *)aReleaseNotesURL;
+
+- (NSString *)DSASignature;
+- (void)setDSASignature:(NSString *)aDSASignature;
+
+- (NSString *)MD5Sum;
+- (void)setMD5Sum:(NSString *)aMd5Sum;
+
+- (NSURL *)fileURL;
+- (void)setFileURL:(NSURL *)aFileURL;
+
+- (NSString *)fileVersion;
+- (void)setFileVersion:(NSString *)aFileVersion;
+
+- (NSString *)versionString;
+- (void)setVersionString:(NSString *)versionString;
+
+- (NSString *)minimumSystemVersion;
+- (void)setMinimumSystemVersion:(NSString *)systemVersionString;
+
+@end
184 SUAppcastItem.m
@@ -0,0 +1,184 @@
+//
+// SUAppcastItem.m
+// Sparkle
+//
+// Created by Andy Matuschak on 3/12/06.
+// Copyright 2006 Andy Matuschak. All rights reserved.
+//
+
+#import "SUAppcastItem.h"
+
+
+@implementation SUAppcastItem
+
+- initWithDictionary:(NSDictionary *)dict
+{
+ [super init];
+ [self setTitle:[dict objectForKey:@"title"]];
+ [self setDate:[dict objectForKey:@"pubDate"]];
+ [self setDescription:[dict objectForKey:@"description"]];
+
+ id enclosure = [dict objectForKey:@"enclosure"];
+ [self setDSASignature:[enclosure objectForKey:@"sparkle:dsaSignature"]];
+ [self setMD5Sum:[enclosure objectForKey:@"sparkle:md5Sum"]];
+
+ [self setFileURL:[NSURL URLWithString:[[enclosure objectForKey:@"url"] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]];
+
+ // Find the appropriate release notes URL.
+ if ([dict objectForKey:@"sparkle:releaseNotesLink"])
+ {
+ [self setReleaseNotesURL:[NSURL URLWithString:[dict objectForKey:@"sparkle:releaseNotesLink"]]];
+ }
+ else if ([[self description] hasPrefix:@"http://"]) // if the description starts with http://, use that.
+ {
+ [self setReleaseNotesURL:[NSURL URLWithString:[self description]]];
+ }
+ else
+ {
+ [self setReleaseNotesURL:nil];
+ }
+
+ NSString *minVersion = [dict objectForKey:@"sparkle:minimumSystemVersion"];
+ if(minVersion)
+ [self setMinimumSystemVersion:minVersion];
+ else
+ [self setMinimumSystemVersion:@"10.3.0"];//sparkle doesn't run on 10.2-, so we don't have to worry about it
+
+ // Try to find a version string.
+ // Finding the new version number from the RSS feed is a little bit hacky. There are two ways:
+ // 1. A "sparkle:version" attribute on the enclosure tag, an extension from the RSS spec.
+ // 2. If there isn't a version attribute, Sparkle will parse the path in the enclosure, expecting
+ // that it will look like this: http://something.com/YourApp_0.5.zip. It'll read whatever's between the last
+ // underscore and the last period as the version number. So name your packages like this: APPNAME_VERSION.extension.
+ // The big caveat with this is that you can't have underscores in your version strings, as that'll confuse Sparkle.
+ // Feel free to change the separator string to a hyphen or something more suited to your needs if you like.
+ NSString *newVersion = [enclosure objectForKey:@"sparkle:version"];
+ if (!newVersion) // no sparkle:version attribute
+ {
+ // Separate the url by underscores and take the last component, as that'll be closest to the end,
+ // then we remove the extension. Hopefully, this will be the version.
+ NSArray *fileComponents = [[enclosure objectForKey:@"url"] componentsSeparatedByString:@"_"];
+ if ([fileComponents count] > 1)
+ newVersion = [[fileComponents lastObject] stringByDeletingPathExtension];
+ }
+ [self setFileVersion:newVersion];
+
+ NSString *shortVersionString = [enclosure objectForKey:@"sparkle:shortVersionString"];
+ if (shortVersionString)
+ {
+ if (![[self fileVersion] isEqualToString:shortVersionString])
+ shortVersionString = [shortVersionString stringByAppendingFormat:@"/%@", [self fileVersion]];
+ [self setVersionString:shortVersionString];
+ }
+ else
+ [self setVersionString:[self fileVersion]];
+
+ return self;
+}
+
+// Attack of accessors!
+
+- (NSString *)title { return [[title retain] autorelease]; }
+
+- (void)setTitle:(NSString *)aTitle
+{
+ [title release];
+ title = [aTitle copy];
+}
+
+
+- (NSDate *)date { return [[date retain] autorelease]; }
+
+- (void)setDate:(NSDate *)aDate
+{
+ [date release];
+ date = [aDate copy];
+}
+
+
+- (NSString *)description { return [[description retain] autorelease]; }
+
+- (void)setDescription:(NSString *)aDescription
+{
+ [description release];
+ description = [aDescription copy];
+}
+
+
+- (NSURL *)releaseNotesURL { return [[releaseNotesURL retain] autorelease]; }
+
+- (void)setReleaseNotesURL:(NSURL *)aReleaseNotesURL
+{
+ [releaseNotesURL release];
+ releaseNotesURL = [aReleaseNotesURL copy];
+}
+
+
+- (NSString *)DSASignature { return [[DSASignature retain] autorelease]; }
+
+- (void)setDSASignature:(NSString *)aDSASignature
+{
+ [DSASignature release];
+ DSASignature = [aDSASignature copy];
+}
+
+
+- (NSString *)MD5Sum { return [[MD5Sum retain] autorelease]; }
+
+- (void)setMD5Sum:(NSString *)aMD5Sum
+{
+ [MD5Sum release];
+ MD5Sum = [aMD5Sum copy];
+}
+
+
+- (NSURL *)fileURL { return [[fileURL retain] autorelease]; }
+
+- (void)setFileURL:(NSURL *)aFileURL
+{
+ [fileURL release];
+ fileURL = [aFileURL copy];
+}
+
+
+- (NSString *)fileVersion { return [[fileVersion retain] autorelease]; }
+
+- (void)setFileVersion:(NSString *)aFileVersion
+{
+ [fileVersion release];
+ fileVersion = [aFileVersion copy];
+}
+
+
+- (NSString *)versionString { return [[versionString retain] autorelease]; }
+
+- (void)setVersionString:(NSString *)aVersionString
+{
+ [versionString release];
+ versionString = [aVersionString copy];
+}
+
+
+- (NSString *)minimumSystemVersion { return [[minimumSystemVersion retain] autorelease]; }
+- (void)setMinimumSystemVersion:(NSString *)systemVersionString
+{
+ [minimumSystemVersion release];
+ minimumSystemVersion = [systemVersionString copy];
+}
+
+
+- (void)dealloc
+{
+ [self setTitle:nil];
+ [self setDate:nil];
+ [self setDescription:nil];
+ [self setReleaseNotesURL:nil];
+ [self setDSASignature:nil];
+ [self setMD5Sum:nil];
+ [self setFileURL:nil];
+ [self setFileVersion:nil];
+ [self setVersionString:nil];
+ [super dealloc];
+}
+
+@end
21 SUAutomaticUpdateAlert.h
@@ -0,0 +1,21 @@
+//
+// SUAutomaticUpdateAlert.h
+// Sparkle
+//
+// Created by Andy Matuschak on 3/18/06.
+// Copyright 2006 Andy Matuschak. All rights reserved.
+//
+
+#import <Cocoa/Cocoa.h>
+
+@class SUAppcastItem;
+@interface SUAutomaticUpdateAlert : NSWindowController {
+ SUAppcastItem *updateItem;
+}
+
+- initWithAppcastItem:(SUAppcastItem *)item;
+
+- (IBAction)relaunchNow:sender;
+- (IBAction)relaunchLater:sender;
+
+@end
61 SUAutomaticUpdateAlert.m
@@ -0,0 +1,61 @@
+//
+// SUAutomaticUpdateAlert.m
+// Sparkle
+//
+// Created by Andy Matuschak on 3/18/06.
+// Copyright 2006 Andy Matuschak. All rights reserved.
+//
+
+#import "SUAutomaticUpdateAlert.h"
+#import "SUUtilities.h"
+#import "SUAppcastItem.h"
+
+@implementation SUAutomaticUpdateAlert
+
+- initWithAppcastItem:(SUAppcastItem *)item
+{
+ NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"SUAutomaticUpdateAlert" ofType:@"nib"];
+ if (!path) // slight hack to resolve issues with running with in configurations
+ {
+ NSBundle *current = [NSBundle bundleForClass:[self class]];
+ NSString *frameworkPath = [[[NSBundle mainBundle] sharedFrameworksPath] stringByAppendingFormat:@"/Sparkle.framework", [current bundleIdentifier]];
+ NSBundle *framework = [NSBundle bundleWithPath:frameworkPath];
+ path = [framework pathForResource:@"SUAutomaticUpdateAlert" ofType:@"nib"];
+ }
+
+ [super initWithWindowNibPath:path owner:self];
+
+ updateItem = [item retain];
+ [self setShouldCascadeWindows:NO];
+
+ return self;
+}
+
+- (IBAction)relaunchNow:sender
+{
+ [self close];
+ [NSApp stopModalWithCode:NSAlertDefaultReturn];
+}
+
+- (IBAction)relaunchLater:sender
+{
+ [self close];
+ [NSApp stopModalWithCode:NSAlertAlternateReturn];
+}
+
+- (NSImage *)applicationIcon
+{
+ return [NSImage imageNamed:@"NSApplicationIcon"];
+}
+
+- (NSString *)titleText
+{
+ return [NSString stringWithFormat:SULocalizedString(@"A new version of %@ has been installed!", nil), SUHostAppDisplayName()];
+}
+
+- (NSString *)descriptionText
+{
+ return [NSString stringWithFormat:SULocalizedString(@"%@ %@ has been installed and will be ready to use next time %@ starts! Would you like to relaunch now?", nil), SUHostAppDisplayName(), [updateItem versionString], SUHostAppDisplayName()];
+}
+
+@end
20 SUConstants.h
@@ -0,0 +1,20 @@
+//
+// SUConstants.h
+// Sparkle
+//
+// Created by Andy Matuschak on 3/16/06.
+// Copyright 2006 Andy Matuschak. All rights reserved.
+//
+
+extern NSString *SUUpdaterWillRestartNotification;
+
+extern NSString *SUCheckAtStartupKey;
+extern NSString *SUFeedURLKey;
+extern NSString *SUShowReleaseNotesKey;
+extern NSString *SUSkippedVersionKey;
+extern NSString *SUScheduledCheckIntervalKey;
+extern NSString *SULastCheckTimeKey;
+extern NSString *SUExpectsDSASignatureKey;
+extern NSString *SUPublicDSAKeyKey;
+extern NSString *SUAutomaticallyUpdateKey;
+extern NSString *SUAllowsAutomaticUpdatesKey;
20 SUConstants.m
@@ -0,0 +1,20 @@
+//
+// SUConstants.m
+// Sparkle
+//
+// Created by Andy Matuschak on 3/16/06.
+// Copyright 2006 Andy Matuschak. All rights reserved.
+//
+
+NSString *SUUpdaterWillRestartNotification = @"SUUpdaterWillRestartNotificationName";
+
+NSString *SUCheckAtStartupKey = @"SUCheckAtStartup";
+NSString *SUFeedURLKey = @"SUFeedURL";
+NSString *SUShowReleaseNotesKey = @"SUShowReleaseNotes";
+NSString *SUSkippedVersionKey = @"SUSkippedVersion";
+NSString *SUScheduledCheckIntervalKey = @"SUScheduledCheckInterval";
+NSString *SULastCheckTimeKey = @"SULastCheckTime";
+NSString *SUExpectsDSASignatureKey = @"SUExpectsDSASignature";
+NSString *SUPublicDSAKeyKey = @"SUPublicDSAKey";
+NSString *SUAutomaticallyUpdateKey = @"SUAutomaticallyUpdate";
+NSString *SUAllowsAutomaticUpdatesKey = @"SUAllowsAutomaticUpdates";
26 SUStatusChecker.h
@@ -0,0 +1,26 @@
+//
+// SUStatusChecker.h
+// Sparkle
+//
+// Created by Evan Schoenberg on 7/6/06.
+//
+
+#import <Cocoa/Cocoa.h>
+#import <Sparkle/SUUpdater.h>
+
+@class SUStatusChecker;
+
+@protocol SUStatusCheckerDelegate <NSObject>
+//versionString will be nil and isNewVersion will be NO if version checking fails.
+- (void)statusChecker:(SUStatusChecker *)statusChecker foundVersion:(NSString *)versionString isNewVersion:(BOOL)isNewVersion;
+@end
+
+@interface SUStatusChecker : SUUpdater {
+ id<SUStatusCheckerDelegate> scDelegate;
+}
+
+// Create a status checker which will notifiy delegate once the appcast version is determined.
+// Notification occurs via the method defined in the SUStatusCheckerDelegate informal protocol.
++ (SUStatusChecker *)statusCheckerForDelegate:(id<SUStatusCheckerDelegate>)delegate;
+
+@end
78 SUStatusChecker.m
@@ -0,0 +1,78 @@
+//
+// SUStatusChecker.m
+// Sparkle
+//
+// Created by Evan Schoenberg on 7/6/06.
+//
+
+#import "SUStatusChecker.h"
+#import "SUAppcast.h"
+#import "SUAppcastItem.h"
+
+@interface SUStatusChecker (Private)
+- (id)initForDelegate:(id<SUStatusCheckerDelegate>)inDelegate;
+- (void)checkForUpdatesAndNotify:(BOOL)verbosity;
+- (BOOL)newVersionAvailable;
+@end;
+
+@implementation SUStatusChecker
+
++ (SUStatusChecker *)statusCheckerForDelegate:(id<SUStatusCheckerDelegate>)inDelegate;
+{
+ SUStatusChecker *statusChecker = [[self alloc] initForDelegate:inDelegate];
+
+ return [statusChecker autorelease];
+}
+
+- (id)initForDelegate:(id<SUStatusCheckerDelegate>)inDelegate
+{
+ [super init];
+
+ scDelegate = [inDelegate retain];
+
+ [self checkForUpdatesAndNotify:NO];
+
+ return self;
+}
+
+- (void)dealloc
+{
+ [scDelegate release]; scDelegate = nil;
+
+ [super dealloc];
+}
+
+- (void)applicationDidFinishLaunching:(NSNotification *)note
+{
+ //Take no action when the application finishes launching
+}
+
+- (void)appcastDidFinishLoading:(SUAppcast *)ac
+{
+ @try
+ {
+ if (!ac) { [NSException raise:@"SUAppcastException" format:@"Couldn't get a valid appcast from the server."]; }
+
+ updateItem = [[ac newestItem] retain];
+ [ac autorelease];
+
+ if (![updateItem fileVersion])
+ {
+ [NSException raise:@"SUAppcastException" format:@"Can't extract a version string from the appcast feed. The filenames should look like YourApp_1.5.tgz, where 1.5 is the version number."];
+ }
+
+ [scDelegate statusChecker:self
+ foundVersion:[updateItem fileVersion]
+ isNewVersion:[self newVersionAvailable]];
+ }
+ @catch (NSException *e)
+ {
+ NSLog([e reason]);
+
+ [scDelegate statusChecker:self foundVersion:nil isNewVersion:NO];
+ }
+
+ updateInProgress = NO;
+}
+
+@end
33 SUStatusController.h
@@ -0,0 +1,33 @@
+//
+// SUStatusController.h
+// Sparkle
+//
+// Created by Andy Matuschak on 3/14/06.
+// Copyright 2006 Andy Matuschak. All rights reserved.
+//
+
+#import <Cocoa/Cocoa.h>
+
+
+@interface SUStatusController : NSWindowController {
+ double progressValue, maxProgressValue;
+ NSString *title, *statusText, *buttonTitle;
+ IBOutlet NSButton *actionButton;
+}
+
+// Pass 0 for the max progress value to get an indeterminate progress bar.
+// Pass nil for the status text to not show it.
+- (void)beginActionWithTitle:(NSString *)title maxProgressValue:(double)maxProgressValue statusText:(NSString *)statusText;
+
+// If isDefault is YES, the button's key equivalent will be \r.
+- (void)setButtonTitle:(NSString *)buttonTitle target:target action:(SEL)action isDefault:(BOOL)isDefault;
+- (void)setButtonEnabled:(BOOL)enabled;
+
+- (double)progressValue;
+- (void)setProgressValue:(double)value;
+- (double)maxProgressValue;
+- (void)setMaxProgressValue:(double)value;
+
+- (void)setStatusText:(NSString *)statusText;
+
+@end
119 SUStatusController.m
@@ -0,0 +1,119 @@
+//
+// SUStatusController.m
+// Sparkle
+//
+// Created by Andy Matuschak on 3/14/06.
+// Copyright 2006 Andy Matuschak. All rights reserved.
+//
+
+#import "SUStatusController.h"
+#import "SUUtilities.h"
+
+@implementation SUStatusController
+
+- init
+{
+ NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"SUStatus" ofType:@"nib"];
+ if (!path) // slight hack to resolve issues with running in debug configurations
+ {
+ NSBundle *current = [NSBundle bundleForClass:[self class]];
+ NSString *frameworkPath = [[[NSBundle mainBundle] sharedFrameworksPath] stringByAppendingFormat:@"/Sparkle.framework", [current bundleIdentifier]];
+ NSBundle *framework = [NSBundle bundleWithPath:frameworkPath];
+ path = [framework pathForResource:@"SUStatus" ofType:@"nib"];
+ }
+ [super initWithWindowNibPath:path owner:self];
+ [self setShouldCascadeWindows:NO];
+ return self;
+}
+
+- (void)dealloc
+{
+ [title release];
+ [statusText release];
+ [buttonTitle release];
+ [super dealloc];
+}
+
+- (void)awakeFromNib
+{
+ [[self window] center];
+ [[self window] setFrameAutosaveName:@"SUStatusFrame"];
+}
+
+- (NSString *)windowTitle
+{
+ return [NSString stringWithFormat:SULocalizedString(@"Updating %@", nil), SUHostAppDisplayName()];
+}
+
+- (NSImage *)applicationIcon
+{
+ return [NSImage imageNamed:@"NSApplicationIcon"];
+}
+
+- (void)beginActionWithTitle:(NSString *)aTitle maxProgressValue:(double)aMaxProgressValue statusText:(NSString *)aStatusText
+{
+ [self willChangeValueForKey:@"title"];
+ title = [aTitle copy];
+ [self didChangeValueForKey:@"title"];
+
+ [self setMaxProgressValue:aMaxProgressValue];
+ [self setStatusText:aStatusText];
+}
+
+- (void)setButtonTitle:(NSString *)aButtonTitle target:target action:(SEL)action isDefault:(BOOL)isDefault
+{
+ [self willChangeValueForKey:@"buttonTitle"];
+ buttonTitle = [aButtonTitle copy];
+ [self didChangeValueForKey:@"buttonTitle"];
+
+ [actionButton sizeToFit];
+ // Except we're going to add 15 px for padding.
+ [actionButton setFrameSize:NSMakeSize([actionButton frame].size.width + 15, [actionButton frame].size.height)];
+ // Now we have to move it over so that it's always 15px from the side of the window.
+ [actionButton setFrameOrigin:NSMakePoint([[self window] frame].size.width - 15 - [actionButton frame].size.width, [actionButton frame].origin.y)];
+ // Redisplay superview to clean up artifacts
+ [[actionButton superview] display];
+
+ [actionButton setTarget:target];
+ [actionButton setAction:action];
+ [actionButton setKeyEquivalent:isDefault ? @"\r" : @""];
+}
+
+- (void)setButtonEnabled:(BOOL)enabled
+{
+ [actionButton setEnabled:enabled];
+}
+
+- (double)progressValue
+{
+ return progressValue;
+}
+
+- (void)setProgressValue:(double)value
+{
+ [self willChangeValueForKey:@"progressValue"];
+ progressValue = value;
+ [self didChangeValueForKey:@"progressValue"];
+}
+
+- (double)maxProgressValue
+{
+ return maxProgressValue;
+}
+
+- (void)setMaxProgressValue:(double)value
+{
+ [self willChangeValueForKey:@"maxProgressValue"];
+ maxProgressValue = value;
+ [self didChangeValueForKey:@"maxProgressValue"];
+ [self setProgressValue:0];
+}
+
+- (void)setStatusText:(NSString *)aStatusText
+{
+ [self willChangeValueForKey:@"statusText"];
+ statusText = [aStatusText copy];
+ [self didChangeValueForKey:@"statusText"];
+}
+
+@end
25 SUUnarchiver.h
@@ -0,0 +1,25 @@
+//
+// SUUnarchiver.h
+// Sparkle
+//
+// Created by Andy Matuschak on 3/16/06.
+// Copyright 2006 Andy Matuschak. All rights reserved.
+//
+
+#import <Cocoa/Cocoa.h>
+
+
+@interface SUUnarchiver : NSObject {
+ id delegate;
+}
+
+- (void)unarchivePath:(NSString *)path;
+- (void)setDelegate:delegate;
+
+@end
+
+@interface NSObject (SUUnarchiverDelegate)
+- (void)unarchiver:(SUUnarchiver *)unarchiver extractedLength:(long)length;
+- (void)unarchiverDidFinish:(SUUnarchiver *)unarchiver;
+- (void)unarchiverDidFail:(SUUnarchiver *)unarchiver;
+@end
144 SUUnarchiver.m
@@ -0,0 +1,144 @@
+//
+// SUUnarchiver.m
+// Sparkle
+//
+// Created by Andy Matuschak on 3/16/06.
+// Copyright 2006 Andy Matuschak. All rights reserved.
+//
+
+#import "SUUnarchiver.h"
+
+
+@implementation SUUnarchiver
+
+// This method abstracts the types that use a command line tool piping data from stdin.
+- (BOOL)_extractArchivePath:archivePath pipingDataToCommand:(NSString *)command
+{
+ // Get the file size.
+ NSNumber *fs = [[[NSFileManager defaultManager] fileAttributesAtPath:archivePath traverseLink:NO] objectForKey:NSFileSize];
+ if (fs == nil) { return NO; }
+
+ // Thank you, Allan Odgaard!
+ // (who wrote the following extraction alg.)
+
+ long current = 0;
+ FILE *fp, *cmdFP;
+ sig_t oldSigPipeHandler = signal(SIGPIPE, SIG_IGN);
+ if ((fp = fopen([archivePath UTF8String], "r")))
+ {
+ setenv("DESTINATION", [[archivePath stringByDeletingLastPathComponent] UTF8String], 1);
+ if ((cmdFP = popen([command cString], "w")))
+ {
+ char buf[32*1024];
+ long len;
+ while((len = fread(buf, 1, 32 * 1024, fp)))
+ {
+ current += len;
+
+ NSEvent *event;
+ while((event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:nil inMode:NSDefaultRunLoopMode dequeue:YES]))
+ [NSApp sendEvent:event];
+
+ fwrite(buf, 1, len, cmdFP);
+
+ if ([delegate respondsToSelector:@selector(unarchiver:extractedLength:)])
+ [delegate unarchiver:self extractedLength:len];
+ }
+ pclose(cmdFP);
+ }
+ fclose(fp);
+ }
+ signal(SIGPIPE, oldSigPipeHandler);
+ return YES;
+}
+
+- (BOOL)_extractTAR:(NSString *)archivePath
+{
+ return [self _extractArchivePath:archivePath pipingDataToCommand:@"tar -xC \"$DESTINATION\""];
+}
+
+- (BOOL)_extractTGZ:(NSString *)archivePath
+{
+ return [self _extractArchivePath:archivePath pipingDataToCommand:@"tar -zxC \"$DESTINATION\""];
+}
+
+- (BOOL)_extractTBZ:(NSString *)archivePath
+{
+ return [self _extractArchivePath:archivePath pipingDataToCommand:@"tar -jxC \"$DESTINATION\""];
+}
+
+- (BOOL)_extractZIP:(NSString *)archivePath
+{
+ return [self _extractArchivePath:archivePath pipingDataToCommand:@"ditto -x -k - \"$DESTINATION\""];
+}
+
+- (BOOL)_extractDMG:(NSString *)archivePath
+{
+ sig_t oldSigChildHandler = signal(SIGCHLD, SIG_DFL);
+ // First, we internet-enable the volume.
+ NSTask *hdiTask = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/env" arguments:[NSArray arrayWithObjects:@"hdiutil", @"internet-enable", @"-quiet", archivePath, nil]];
+ [hdiTask waitUntilExit];
+ if ([hdiTask terminationStatus] != 0) { return NO; }
+
+ // Now, open the volume; it'll extract into its own directory.
+ hdiTask = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/env" arguments:[NSArray arrayWithObjects:@"hdiutil", @"attach", @"-idme", @"-noidmereveal", @"-noidmetrash", @"-noverify", @"-nobrowse", @"-noautoopen", @"-quiet", archivePath, nil]];
+ [hdiTask waitUntilExit];
+ if ([hdiTask terminationStatus] != 0) { return NO; }
+
+ signal(SIGCHLD, oldSigChildHandler);
+
+ return YES;
+}
+
+- (void)_unarchivePath:(NSString *)path
+{
+ NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
+