/core/externals/update-engine/Core/KSTicketStore.m
Objective C | 248 lines | 150 code | 49 blank | 49 comment | 27 complexity | 4ec1881d3c8c5b93e128797988e8d43b MD5 | raw file
1// Copyright 2008 Google Inc. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15#import "KSTicketStore.h" 16#import "KSTicket.h" 17#import <fcntl.h> 18#import <unistd.h> 19 20 21// Implementation note: Tickets are stored internally in a dictionary 22// keyed by each ticket's ProductID. This means that no two tickets 23// can have the same ProductID, which is exactly the behavior that we 24// want. 25 26 27@interface KSTicketStore (PrivateMethods) 28- (NSMutableDictionary *)readTicketMap; 29- (BOOL)writeTicketMap:(NSMutableDictionary *)ticketMap; 30- (NSMutableDictionary *)atomicReadTicketMap; 31- (BOOL)atomicStoreTicket:(KSTicket *)ticket; 32- (BOOL)atomicDeleteTicket:(KSTicket *)ticket; 33@end 34 35 36@implementation KSTicketStore 37 38+ (id)ticketStoreWithPath:(NSString *)path { 39 return [[[self alloc] initWithPath:path] autorelease]; 40} 41 42- (id)initWithPath:(NSString *)path { 43 if ((self = [super init])) { 44 path_ = [path copy]; 45 if (path_ == nil) { 46 [self release]; 47 return nil; 48 } 49 } 50 return self; 51} 52 53- (id)init { 54 return [self initWithPath:nil]; 55} 56 57- (void)dealloc { 58 [path_ release]; 59 [super dealloc]; 60} 61 62- (NSString *)description { 63 return [NSString stringWithFormat:@"<%@:%p path=%@>", 64 [self class], self, path_]; 65} 66 67- (NSString *)path { 68 return path_; 69} 70 71- (int)ticketCount { 72 return [[self tickets] count]; 73} 74 75// We never want to return nil here. If we don't have any tickets, then we'll 76// set an empty array of them, and return that empty array. 77- (NSArray *)tickets { 78 NSDictionary *map = [self atomicReadTicketMap]; 79 return map ? [map allValues] : [NSArray array]; 80} 81 82- (KSTicket *)ticketForProductID:(NSString *)productid { 83 if (productid == nil) return nil; 84 productid = [productid lowercaseString]; 85 KSTicket *ticket = nil; 86 NSEnumerator *ticketEnumerator = [[self tickets] objectEnumerator]; 87 while ((ticket = [ticketEnumerator nextObject])) { 88 // Do case insensitive but case preserving comparison by lowercasing 89 if ([productid isEqualToString:[[ticket productID] lowercaseString]]) 90 break; 91 } 92 return ticket; 93} 94 95- (BOOL)storeTicket:(KSTicket *)ticket { 96 return [self atomicStoreTicket:ticket]; 97} 98 99- (BOOL)deleteTicket:(KSTicket *)ticket { 100 return [self atomicDeleteTicket:ticket]; 101} 102 103- (BOOL)deleteTicketForProductID:(NSString *)productid { 104 KSTicket *ticket = [self ticketForProductID:productid]; 105 return [self deleteTicket:ticket]; 106} 107 108@end // KSTicketStore 109 110 111@implementation KSTicketStore (PrivateMethods) 112 113- (NSMutableDictionary *)readTicketMap { 114 _GTMDevAssert(path_ != nil, @"path_ must not be nil"); 115 NSData *data = [NSData dataWithContentsOfFile:path_]; 116 if ([data length] == 0) return nil; 117 NSDictionary *map = nil; 118 119 @try { 120 map = [NSKeyedUnarchiver unarchiveObjectWithData:data]; 121 // COV_NF_START 122 } 123 @catch (id ex) { 124 GTMLoggerError(@"Caught exception unarchiving ticket store at %@: %@", 125 path_, ex); 126 } 127 // COV_NF_END 128 129 return [[map mutableCopy] autorelease]; 130} 131 132- (BOOL)writeTicketMap:(NSMutableDictionary *)ticketMap { 133 _GTMDevAssert(path_ != nil, @"path_ must not be nil"); 134 if (ticketMap == nil) return NO; 135 BOOL ok = NO; 136 137 @try { 138 ok = [NSKeyedArchiver archiveRootObject:ticketMap toFile:path_]; 139 // COV_NF_START 140 } 141 @catch (id ex) { 142 GTMLoggerError(@"Caught exception archiving ticket map to %@: %@", 143 path_, ex); 144 } 145 // COV_NF_END 146 147 return ok; 148} 149 150// Locking 151// 152// The next few "atomic" methods use these macros to lock the ticket store file 153// at path_ before accessing it. We do this with a simple advisory lock. The 154// lock file itself will be |path_| with a file extension of ".lock". 155// 156// Also, we do this using macros to make clear the very simple nature of this 157// locking mechanism: these locks are not recursive, and aren't smart enough to 158// "NOP" if the process already has the lock. If we wrapped this logic up in a 159// full-blown object, one may be led to think that the locking mechanism is much 160// more sophisticated than it really is. 161// 162// The LOCK_STORE macro simple opens the ticket store path_ with the O_EXLOCK 163// flag set. If this fails, it returns NO. The UNLOCK_STORE() macro simply 164// closes the fd. These two methods must be balanced, and there must be no code 165// paths that could lead to a LOCK_STORE() being called w/o a corresponding 166// UNLOCK_STORE(). Note that since these locks are based on FDs, when the 167// process exists (even if it crashes), the advisory locks will be released. 168#define LOCK_STORE() \ 169 NSString *_lockPath_ = [path_ stringByAppendingPathExtension:@"lock"]; \ 170 int _oflags_ = O_CREAT | O_RDONLY | O_EXLOCK; \ 171 int _lockFD_ = open([_lockPath_ fileSystemRepresentation], _oflags_, 0444); \ 172 if (_lockFD_ < 0) return NO // COV_NF_LINE 173 174#define UNLOCK_STORE() \ 175 if (_lockFD_ >= 0) close(_lockFD_) 176 177- (NSMutableDictionary *)atomicReadTicketMap { 178 LOCK_STORE(); 179 NSMutableDictionary *map = [self readTicketMap]; 180 UNLOCK_STORE(); 181 return map; 182} 183 184// Takes the lock and adds |ticket| to the on-disk ticket store. 185- (BOOL)atomicStoreTicket:(KSTicket *)ticket { 186 _GTMDevAssert(path_ != nil, @"path_ must not be nil"); 187 if (ticket == nil) return NO; 188 189 LOCK_STORE(); 190 191 NSMutableDictionary *map = [self readTicketMap]; 192 if (map == nil) map = [NSMutableDictionary dictionary]; 193 // Store in a case insensitive but case preserving manner 194 [map setObject:ticket forKey:[[ticket productID] lowercaseString]]; 195 BOOL ok = [self writeTicketMap:map]; 196 197 UNLOCK_STORE(); 198 199 return ok; 200} 201 202// Takes the lock and deletes |ticket| from the on-disk ticket store. 203- (BOOL)atomicDeleteTicket:(KSTicket *)ticket { 204 _GTMDevAssert(path_ != nil, @"path_ must not be nil"); 205 if (ticket == nil) return NO; 206 207 LOCK_STORE(); 208 209 NSMutableDictionary *map = [self readTicketMap]; 210 // Delete in a case insensitive manner 211 [map removeObjectForKey:[[ticket productID] lowercaseString]]; 212 BOOL ok = [self writeTicketMap:map]; 213 214 UNLOCK_STORE(); 215 216 return ok; 217} 218 219@end // PrivateMethods 220 221 222@implementation NSArray (TicketRetrieving) 223 224- (NSDictionary *)ticketsByURL { 225 NSMutableDictionary *urlToTickets = [NSMutableDictionary dictionary]; 226 227 KSTicket *ticket = nil; 228 NSEnumerator *ticketEnumerator = [self objectEnumerator]; 229 while ((ticket = [ticketEnumerator nextObject])) { 230 // Return nil if we find an object in the array that's not a KSTicket 231 if (![ticket isKindOfClass:[KSTicket class]]) { 232 // COV_NF_START 233 GTMLoggerError(@"%@ is not a KSTicket, returning nil", ticket); 234 return nil; 235 // COV_NF_END 236 } 237 NSURL *key = [ticket serverURL]; 238 NSMutableArray *tickets = [urlToTickets objectForKey:key]; 239 if (tickets == nil) tickets = [NSMutableArray array]; 240 [tickets addObject:ticket]; 241 [urlToTickets setObject:tickets forKey:key]; 242 } 243 244 return [urlToTickets count] > 0 ? urlToTickets : nil; 245} 246 247@end // TicketStoreQuerying 248