pax_global_header00006660000000000000000000000064144774012110014513gustar00rootroot0000000000000052 comment=f6671447d0512194ea3154128ee34c794def6670 Proton-API-Bridge-1.0.0/000077500000000000000000000000001447740121100145735ustar00rootroot00000000000000Proton-API-Bridge-1.0.0/.github/000077500000000000000000000000001447740121100161335ustar00rootroot00000000000000Proton-API-Bridge-1.0.0/.github/workflows/000077500000000000000000000000001447740121100201705ustar00rootroot00000000000000Proton-API-Bridge-1.0.0/.github/workflows/check.yml000066400000000000000000000010721447740121100217700ustar00rootroot00000000000000name: Lint and Test on: push jobs: check: runs-on: ubuntu-latest steps: - name: Get sources uses: actions/checkout@v3 - name: Set up Go 1.18 uses: actions/setup-go@v3 with: go-version: '1.18' - name: Run golangci-lint uses: golangci/golangci-lint-action@v3 with: version: v1.50.0 args: --timeout=180s skip-cache: true # - name: Run tests # run: go test -v ./... # - name: Run tests with race check # run: go test -v -race ./... Proton-API-Bridge-1.0.0/.gitignore000066400000000000000000000000641447740121100165630ustar00rootroot00000000000000.DS_Store .credential .*.credential data config.tomlProton-API-Bridge-1.0.0/Documentation.md000066400000000000000000000014321447740121100177260ustar00rootroot00000000000000# Documentation Since the Proton API isn't open sourced, this document serves as the team's understanding for future reference. # Proton Drive API ## Terminology ### Volume ### Share ### Node ### Link ## Encryption Encryption, decryption, and signature signing and verification, etc. are all performed by using the go-crypto library. ### Login Proton uses SRP for logging in the users. After logging in, there is a small time window (several minutes) where users can access certain routes, which is in the `scope` field, e.g. getting user salt. Since the user and address key rings are encrypted with passphrase tied to salt and user password, we need to cache this information as soon as the first log in happens for future usage. ### User Key ### Address Key ### Node/Link KeyProton-API-Bridge-1.0.0/LICENSE000066400000000000000000000020721447740121100156010ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2023 Chun-Hung Tseng 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. Proton-API-Bridge-1.0.0/README.md000066400000000000000000000202651447740121100160570ustar00rootroot00000000000000# Proton API Bridge Thanks to Proton open sourcing [proton-go-api](https://github.com/ProtonMail/go-proton-api) and the web, iOS, and Android client codebases, we don't need to completely reverse engineer the APIs by observing the web client traffic! [proton-go-api](https://github.com/ProtonMail/go-proton-api) provides the basic building blocks of API calls and error handling, such as 429 exponential back-off, but it is pretty much just a barebone interface to the Proton API. For example, the encryption and decryption of the Proton Drive file are not provided in this library. This codebase, Proton API Bridge, bridges the gap, so software like [rclone](https://github.com/rclone/rclone) can be built on top of this quickly. This codebase handles the intricate tasks before and after calling Proton APIs, particularly the complex encryption scheme, allowing developers to implement features for other software on top of this codebase. Currently, only Proton Drive APIs are bridged, as we are aiming to implement a backend for rclone. ## Sidenotes We are using a fork of the [proton-go-api](https://github.com/henrybear327/go-proton-api), as we are adding quite some new code to it. We are actively rebasing on top of the master branch of the upstream, as we will try to commit back to the upstream once we feel like the code changes are stable. # Unit testing and linting `golangci-lint run && go test -race -failfast -v ./...` # Drive APIs > In collaboration with Azimjon Pulatov, in memory of our good old days at Meta, London, in the summer of 2022. > > Thanks to Anson Chen for the motivation and some initial help on various matters! Currently, the development are split into 2 versions. V1 supports the features [required by rclone](https://github.com/henrybear327/rclone/blob/master/fs/types.go), such as `file listing`. As the unit and integration tests from rclone have all been passed, we would stabilize this and then move onto developing V2. V2 will bring in optimizations and enhancements, esp. supporting thumbnails. Please see the list below. ## V1 ### Features - [x] Log in to an account without 2FA using username and password - [x] Obtain keyring - [x] Cache access token, etc. to be able to reuse the session - [x] Bug: 403: Access token does not have sufficient scope - used the wrong newClient function - [x] Volume actions - [x] List all volumes - [x] Share actions - [x] Get all shares - [x] Get default share - [x] Fix context with proper propagation instead of using `ctx` everywhere - [x] Folder actions - [x] List all folders and files within the root folder - [x] BUG: listing directory - missing signature when there are more than 1 share -> we need to check for the "active" folder type first - [x] List all folders and files recursively within the root folder - [x] Delete - [x] Create - [x] (Feature) Update - [x] (Feature) Move - [x] File actions - [x] Download - [x] Download empty file - [x] Improve large file download handling - [x] Properly handle large files and empty files (check iOS codebase) - esp. large files, where buffering in-memory will screw up the runtime - [x] Check signature and hash - [x] Delete - [x] Upload - [x] Handle empty file - [x] Parse mime type - [x] Add revision - [x] Modified time - [x] Handle failed / interrupted upload - [x] List file metadata - [x] Duplicated file name handling: 422: A file or folder with that name already exists (Code=2500, Status=422) - [x] Init ProtonDrive with config passed in as Map - [x] Remove all `log.Fatalln` and use proper error propagation (basically remove `HandleError` and we go from there) - [x] Integration tests - [x] Remove drive demo code - [x] Create a Drive struct to encapsulate all the functions (maybe?) - [x] Move comments to proper places - [x] Modify `shouldRejectDestructiveActions()` - [x] Refactor - [x] Reduce config options on caching access token - [x] Remove integration test safeguarding ### TODO - [x] address go dependencies - Fixed by doing the following in the `go-proton-api` repo to bump to use the latest commit - `go get github.com/ProtonMail/go-proton-api@ea8de5f674b7f9b0cca8e3a5076ffe3c5a867e01` - `go get github.com/ProtonMail/gluon@fb7689b15ae39c3efec3ff3c615c3d2dac41cec8` - [x] Remove mail-related apis (to reduce dependencies) - [x] Make a "super class" and expose all necessary methods for the outside to call - [x] Add 2FA login - [x] Fix the function argument passing (using pointers) - [x] Handle account with - [x] multiple addresses - [x] multiple keys per addresses - [x] Update RClone's contribution.md file - [x] Remove delete all's hardcoded string - [x] Point to the right proton-go-api branch - [x] Run `go get github.com/henrybear327/go-proton-api@dev` to update go mod - [x] Pass in AppVersion as a config option - [x] Proper error handling by looking at the return code instead of the error string - [x] Duplicated folder name handling: 422: A file or folder with that name already exists (Code=2500, Status=422) - [x] Not found: ERROR RESTY 422: File or folder was not found. (Code=2501, Status=422), Attempt 1 - [x] Failed upload: Draft already exists on this revision (Code=2500, Status=409) - [x] Fix file upload progress -> If the upload failed, please Replace file. If the upload is still in progress, replacing it will cancel the ongoing upload - [x] Concurrency control on file encryption, decryption, and block upload ### Known limitations - No thumbnails, respecting accepted MIME types, max upload size, can't init Proton Drive, etc. - Assumptions - only one main share per account - only operate on active links ## V2 - [ ] Support thumbnail - [ ] Potential bugs - [ ] Confirm the HMAC algorithm -> if you create a draft using integration test, and then use the web frontend to finish the upload (you will see overwrite pop-up), and then use the web frontend to upload again the same file, but this time you will have 2 files with duplicated names - [ ] Might have missing signature issues on some old accounts, e.g. GetHashKey on rootLink might fail -> currently have a quick patch, but might need to double check the behavior - [ ] Double check the attrs field parsing, esp. for size - [ ] Double check the attrs field, esp. for size - [ ] Crypto-related operations, e.g. signature verification, still needs to cross check with iOS or web open source codebase - [ ] Mimetype detection by [using the file content itself](github.com/gabriel-vasile/mimetype), or Google content sniffer - [ ] Remove e.g. proton.link related exposures in the function signature (this library should abstract them all) - [ ] Improve documentation - [ ] Go through Drive iOS source code and check the logic control flow - [ ] File - [ ] Parallel download / upload -> enc/dec is expensive - [ ] [Filename encoding](https://github.com/ProtonMail/WebClients/blob/b4eba99d241af4fdae06ff7138bd651a40ef5d3c/applications/drive/src/app/store/_links/validation.ts#L51) - [ ] Commit back to proton-go-api and switch to using upstream (make sure the tag is at the tip though) - [ ] Support legacy 2-password mode - [ ] Proton Drive init (no prior Proton Drive login before -> probably will have no key, volume, etc. to start with at all) - [ ] linkID caching -> would need to listen to the event api though - [ ] Integration tests - [ ] Check file metadata - [ ] Try to check if all functions are used at least once so we know if it's functioning or not - [ ] Handle accounts with multiple shares - [ ] Use CI to run integration tests - [ ] Some error handling from [here](https://github.com/ProtonMail/WebClients/blob/main/packages/shared/lib/drive/constants.ts) MAX_NAME_LENGTH, TIMEOUT - [ ] [Mimetype restrictions](https://github.com/ProtonMail/WebClients/blob/main/packages/shared/lib/drive/constants.ts#LL47C14-L47C42) - [ ] Address TODO and FIXME # Questions - [x] rclone's folder / file rename detection? -> just implement the interface and rclone will deal with the rest! # Notes - Due to caching, functions using `...ByID` needs to perform `protonDrive.removeLinkIDFromCache(linkID, false)` in order to get the latest data! Proton-API-Bridge-1.0.0/cache.go000066400000000000000000000174011447740121100161700ustar00rootroot00000000000000package proton_api_bridge import ( "context" "log" "sync" "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/henrybear327/go-proton-api" ) type cacheEntry struct { link *proton.Link kr *crypto.KeyRing } type cache struct { data map[string]*cacheEntry children map[string]map[string]interface{} enableCaching bool sync.RWMutex } func newCache(enableCaching bool) *cache { return &cache{ data: make(map[string]*cacheEntry), children: make(map[string]map[string]interface{}), enableCaching: enableCaching, } } // func (cache *cache) _debug() { // if !cache.enableCaching { // return // } // cache.RLock() // defer cache.RUnlock() // for k, v := range cache.data { // log.Printf("data %#v %p", k, v.link) // } // for k, v := range cache.children { // log.Printf("children %#v %#v", k, v) // } // } func (cache *cache) _get(linkID string) *cacheEntry { if !cache.enableCaching { return nil } cache.RLock() defer cache.RUnlock() if data, ok := cache.data[linkID]; ok { return data } return nil } func (cache *cache) _insert(linkID string, link *proton.Link, kr *crypto.KeyRing) { if !cache.enableCaching { return } cache.Lock() defer cache.Unlock() cache.data[linkID] = &cacheEntry{ link: link, kr: kr, } if link != nil { if data, ok := cache.children[link.ParentLinkID]; ok { data[link.LinkID] = nil cache.children[link.ParentLinkID] = data } else { tmp := make(map[string]interface{}) tmp[link.LinkID] = nil cache.children[link.ParentLinkID] = tmp } } else { // TODO: we should never have missing link though log.Fatalln("we should never have missing link though") } } // due to recursion, we can't perform locking here // this function should only be called from _remove func (cache *cache) _remove_nolock(linkID string, includingChildren bool) { var link *proton.Link if data, ok := cache.data[linkID]; ok { link = data.link delete(cache.data, linkID) } else { return } // remove linkID from parent's map if data, ok := cache.children[link.ParentLinkID]; ok { if _, ok := data[link.LinkID]; ok { delete(data, link.LinkID) cache.children[link.ParentLinkID] = data } else { log.Fatalln("we have an issue for cache inconsistency where link is not found in the parent's map") } } else { log.Fatalln("we have an issue for cache inconsistency where link's parent map is missing") } // we don't recursively go upward to clean up the parent's map // instead, we rely on periodic cache flushing if includingChildren { if data, ok := cache.children[link.LinkID]; ok { for k := range data { cache._remove_nolock(k, true) } } // else { // might have nothing is the link doesn't have any children // } delete(cache.children, link.LinkID) } } func (cache *cache) _remove(linkID string, includingChildren bool) { if !cache.enableCaching { return } cache.Lock() defer cache.Unlock() cache._remove_nolock(linkID, includingChildren) } /* The original non-caching version, which resolves the keyring recursively */ func (protonDrive *ProtonDrive) _getLinkKRByID(ctx context.Context, linkID string) (*crypto.KeyRing, error) { if linkID == "" { // most likely someone requested root link's parent link, which happen to be "" // return protonDrive.MainShareKR.Copy() // we need to return a deep copy since the keyring will be freed by the caller when it finishes using the keyring -> now we go through caching, so we won't clear kr return protonDrive.MainShareKR, nil } link, err := protonDrive.getLink(ctx, linkID) if err != nil { return nil, err } return protonDrive._getLinkKR(ctx, link) } /* The original non-caching version, which resolves the keyring recursively */ func (protonDrive *ProtonDrive) _getLinkKR(ctx context.Context, link *proton.Link) (*crypto.KeyRing, error) { if link.ParentLinkID == "" { // link is rootLink signatureVerificationKR, err := protonDrive.getSignatureVerificationKeyring([]string{link.SignatureEmail}) if err != nil { return nil, err } nodeKR, err := link.GetKeyRing(protonDrive.MainShareKR, signatureVerificationKR) if err != nil { return nil, err } return nodeKR, nil } parentLink, err := protonDrive.getLink(ctx, link.ParentLinkID) if err != nil { return nil, err } // parentNodeKR is used to decrypt the current node's KR, as each node has its keyring, which can be decrypted by its parent parentNodeKR, err := protonDrive._getLinkKR(ctx, parentLink) if err != nil { return nil, err } signatureVerificationKR, err := protonDrive.getSignatureVerificationKeyring([]string{link.SignatureEmail}) if err != nil { return nil, err } nodeKR, err := link.GetKeyRing(parentNodeKR, signatureVerificationKR) if err != nil { return nil, err } return nodeKR, nil } func (protonDrive *ProtonDrive) getLink(ctx context.Context, linkID string) (*proton.Link, error) { if linkID == "" { // this is only possible when doing rootLink's parent, which should be handled beforehand return nil, ErrWrongUsageOfGetLink } // attempt to get from cache first if data := protonDrive.cache._get(linkID); data != nil && data.link != nil { return data.link, nil } // no cached data, fetch link, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, linkID) if err != nil { return nil, err } // populate cache protonDrive.cache._insert(linkID, &link, nil) return &link, nil } func (protonDrive *ProtonDrive) getLinkKR(ctx context.Context, link *proton.Link) (*crypto.KeyRing, error) { if !protonDrive.cache.enableCaching { return protonDrive._getLinkKR(ctx, link) } if link == nil { return nil, ErrWrongUsageOfGetLinkKR } // attempt to get from cache first if data := protonDrive.cache._get(link.LinkID); data != nil && data.link != nil { if data.kr != nil { return data.kr, nil } // decrypt keyring and cache it parentNodeKR, err := protonDrive.getLinkKRByID(ctx, data.link.ParentLinkID) if err != nil { return nil, err } signatureVerificationKR, err := protonDrive.getSignatureVerificationKeyring([]string{data.link.SignatureEmail}) if err != nil { return nil, err } kr, err := data.link.GetKeyRing(parentNodeKR, signatureVerificationKR) if err != nil { return nil, err } data.kr = kr return data.kr, nil } // no cached data, fetch protonDrive.cache._insert(link.LinkID, link, nil) return protonDrive.getLinkKR(ctx, link) } func (protonDrive *ProtonDrive) getLinkKRByID(ctx context.Context, linkID string) (*crypto.KeyRing, error) { if !protonDrive.cache.enableCaching { return protonDrive._getLinkKRByID(ctx, linkID) } if linkID == "" { return protonDrive.MainShareKR, nil } // attempt to get from cache first if data := protonDrive.cache._get(linkID); data != nil && data.link != nil { return protonDrive.getLinkKR(ctx, data.link) } // log.Println("Not from cache") // no cached data, fetch link, err := protonDrive.getLink(ctx, linkID) if err != nil { return nil, err } return protonDrive.getLinkKR(ctx, link) } func (protonDrive *ProtonDrive) removeLinkIDFromCache(linkID string, includingChildren bool) { if !protonDrive.cache.enableCaching { return } // log.Println("===================================") // log.Println(linkID, includingChildren) // protonDrive.cache._debug() protonDrive.cache._remove(linkID, includingChildren) // protonDrive.cache._debug() // log.Println("===================================") } func (protonDrive *ProtonDrive) ClearCache() { if !protonDrive.cache.enableCaching { return } protonDrive.cache.Lock() defer protonDrive.cache.Unlock() // TODO: we come back to fix this later // for _, entry := range protonDrive.cache.data { // entry.kr.ClearPrivateParams() // } protonDrive.cache.data = make(map[string]*cacheEntry) protonDrive.cache.children = make(map[string]map[string]interface{}) } Proton-API-Bridge-1.0.0/cache_test.go000066400000000000000000000000321447740121100172170ustar00rootroot00000000000000package proton_api_bridge Proton-API-Bridge-1.0.0/common/000077500000000000000000000000001447740121100160635ustar00rootroot00000000000000Proton-API-Bridge-1.0.0/common/config.go000066400000000000000000000066471447740121100176740ustar00rootroot00000000000000package common import ( "os" "runtime" ) type Config struct { /* Constants */ AppVersion string UserAgent string /* Login */ FirstLoginCredential *FirstLoginCredentialData ReusableCredential *ReusableCredentialData UseReusableLogin bool CredentialCacheFile string // If CredentialCacheFile is empty, no credential will be logged /* Setting */ DestructiveIntegrationTest bool // CAUTION: the integration test requires a clean proton drive EmptyTrashAfterIntegrationTest bool // CAUTION: the integration test will clean up all the data in the trash ReplaceExistingDraft bool // for the file upload replace or keep it as-is option EnableCaching bool // link node caching ConcurrentBlockUploadCount int ConcurrentFileCryptoCount int /* Drive */ DataFolderName string } type FirstLoginCredentialData struct { Username string Password string MailboxPassword string TwoFA string } type ReusableCredentialData struct { UID string AccessToken string RefreshToken string SaltedKeyPass string // []byte <-> base64 } func NewConfigWithDefaultValues() *Config { return &Config{ AppVersion: "", UserAgent: "", FirstLoginCredential: &FirstLoginCredentialData{ Username: "", Password: "", MailboxPassword: "", TwoFA: "", }, ReusableCredential: &ReusableCredentialData{ UID: "", AccessToken: "", RefreshToken: "", SaltedKeyPass: "", // []byte <-> base64 }, UseReusableLogin: false, CredentialCacheFile: "", DestructiveIntegrationTest: false, EmptyTrashAfterIntegrationTest: false, ReplaceExistingDraft: false, EnableCaching: true, ConcurrentBlockUploadCount: 20, // let's be a nice citizen and not stress out proton engineers :) ConcurrentFileCryptoCount: runtime.GOMAXPROCS(0), DataFolderName: "data", } } func NewConfigForIntegrationTests() *Config { appVersion := os.Getenv("PROTON_API_BRIDGE_APP_VERSION") userAgent := os.Getenv("PROTON_API_BRIDGE_USER_AGENT") username := os.Getenv("PROTON_API_BRIDGE_TEST_USERNAME") password := os.Getenv("PROTON_API_BRIDGE_TEST_PASSWORD") twoFA := os.Getenv("PROTON_API_BRIDGE_TEST_TWOFA") useReusableLoginStr := os.Getenv("PROTON_API_BRIDGE_TEST_USE_REUSABLE_LOGIN") useReusableLogin := false if useReusableLoginStr == "1" { useReusableLogin = true } uid := os.Getenv("PROTON_API_BRIDGE_TEST_UID") accessToken := os.Getenv("PROTON_API_BRIDGE_TEST_ACCESS_TOKEN") refreshToken := os.Getenv("PROTON_API_BRIDGE_TEST_REFRESH_TOKEN") saltedKeyPass := os.Getenv("PROTON_API_BRIDGE_TEST_SALTEDKEYPASS") return &Config{ AppVersion: appVersion, UserAgent: userAgent, FirstLoginCredential: &FirstLoginCredentialData{ Username: username, Password: password, MailboxPassword: "", TwoFA: twoFA, }, ReusableCredential: &ReusableCredentialData{ UID: uid, AccessToken: accessToken, RefreshToken: refreshToken, SaltedKeyPass: saltedKeyPass, // []byte <-> base64 }, UseReusableLogin: useReusableLogin, CredentialCacheFile: ".credential", DestructiveIntegrationTest: true, EmptyTrashAfterIntegrationTest: true, ReplaceExistingDraft: false, EnableCaching: true, ConcurrentBlockUploadCount: 20, ConcurrentFileCryptoCount: runtime.GOMAXPROCS(0), DataFolderName: "data", } } Proton-API-Bridge-1.0.0/common/error.go000066400000000000000000000010171447740121100175420ustar00rootroot00000000000000package common import "errors" var ( ErrKeyPassOrSaltedKeyPassMustBeNotNil = errors.New("either keyPass or saltedKeyPass must be not nil") ErrFailedToUnlockUserKeys = errors.New("failed to unlock user keys") ErrUsernameAndPasswordRequired = errors.New("username and password are required") Err2FACodeRequired = errors.New("this account requires a 2FA code. Can be provided with --protondrive-2fa=000000") ErrMailboxPasswordRequired = errors.New("this account requires a mailbox password") ) Proton-API-Bridge-1.0.0/common/keyring.go000066400000000000000000000041441447740121100200650ustar00rootroot00000000000000package common import ( "context" "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/henrybear327/go-proton-api" ) /* The Proton account keys are organized in the following hierarchy. An account has some users, each of the user will have one or more user keys. Each of the user will have some addresses, each of the address will have one or more address keys. A key is encrypted by a passphrase, and the passphrase is encrypted by another key. The address keyrings are encrypted with the primary user keyring at the time. The primary address key is used to create (encrypt) and retrieve (decrypt) data, e.g. shares */ func getAccountKRs(ctx context.Context, c *proton.Client, keyPass, saltedKeyPass []byte) (*crypto.KeyRing, map[string]*crypto.KeyRing, map[string]proton.Address, []byte, error) { /* Code taken and modified from proton-bridge */ user, err := c.GetUser(ctx) if err != nil { return nil, nil, nil, nil, err } // log.Printf("user %#v", user) addrsArr, err := c.GetAddresses(ctx) if err != nil { return nil, nil, nil, nil, err } // log.Printf("addr %#v", addr) if saltedKeyPass == nil { if keyPass == nil { return nil, nil, nil, nil, ErrKeyPassOrSaltedKeyPassMustBeNotNil } /* Notes for -> BUG: Access token does not have sufficient scope Only within the first x minutes that the user logs in with username and password, the getSalts route will be available to be called! */ salts, err := c.GetSalts(ctx) if err != nil { return nil, nil, nil, nil, err } // log.Printf("salts %#v", salts) saltedKeyPass, err = salts.SaltForKey(keyPass, user.Keys.Primary().ID) if err != nil { return nil, nil, nil, nil, err } // log.Printf("saltedKeyPass ok") } userKR, addrKRs, err := proton.Unlock(user, addrsArr, saltedKeyPass, nil) if err != nil { return nil, nil, nil, nil, err } else if userKR.CountDecryptionEntities() == 0 { if err != nil { return nil, nil, nil, nil, ErrFailedToUnlockUserKeys } } addrs := make(map[string]proton.Address) for _, addr := range addrsArr { addrs[addr.Email] = addr } return userKR, addrKRs, addrs, saltedKeyPass, nil } Proton-API-Bridge-1.0.0/common/proton_manager.go000066400000000000000000000006041447740121100214250ustar00rootroot00000000000000package common import ( "github.com/henrybear327/go-proton-api" ) func getProtonManager(appVersion string, userAgent string) *proton.Manager { /* Notes on API calls: if the app version is not specified, the api calls will be rejected. */ options := []proton.Option{ proton.WithAppVersion(appVersion), proton.WithUserAgent(userAgent), } m := proton.New(options...) return m } Proton-API-Bridge-1.0.0/common/user.go000066400000000000000000000110021447740121100173620ustar00rootroot00000000000000package common import ( "context" "encoding/base64" "encoding/json" "log" "os" "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/henrybear327/go-proton-api" ) type ProtonDriveCredential struct { UID string AccessToken string RefreshToken string SaltedKeyPass string } func cacheCredentialToFile(config *Config) error { if config.CredentialCacheFile != "" { str, err := json.Marshal(config.ReusableCredential) if err != nil { return err } file, err := os.Create(config.CredentialCacheFile) if err != nil { return err } defer file.Close() _, err = file.WriteString(string(str)) if err != nil { return err } } return nil } /* Log in methods - username and password to log in - UID and refresh token Keyring decryption The password will be salted, and then used to decrypt the keyring. The salted password needs to be and can be cached, so the keyring can be re-decrypted when needed */ func Login(ctx context.Context, config *Config, authHandler proton.AuthHandler, deAuthHandler proton.Handler) (*proton.Manager, *proton.Client, *ProtonDriveCredential, *crypto.KeyRing, map[string]*crypto.KeyRing, map[string]proton.Address, error) { var c *proton.Client var auth proton.Auth var userKR *crypto.KeyRing var addrKRs map[string]*crypto.KeyRing var addrs map[string]proton.Address // get manager m := getProtonManager(config.AppVersion, config.UserAgent) if config.UseReusableLogin { c = m.NewClient(config.ReusableCredential.UID, config.ReusableCredential.AccessToken, config.ReusableCredential.RefreshToken) c.AddAuthHandler(authHandler) c.AddDeauthHandler(deAuthHandler) err := cacheCredentialToFile(config) if err != nil { return nil, nil, nil, nil, nil, nil, err } SaltedKeyPassByteArr, err := base64.StdEncoding.DecodeString(config.ReusableCredential.SaltedKeyPass) if err != nil { return nil, nil, nil, nil, nil, nil, err } userKR, addrKRs, addrs, _, err = getAccountKRs(ctx, c, nil, SaltedKeyPassByteArr) if err != nil { return nil, nil, nil, nil, nil, nil, err } return m, c, nil, userKR, addrKRs, addrs, nil } else { username := config.FirstLoginCredential.Username password := config.FirstLoginCredential.Password if username == "" || password == "" { return nil, nil, nil, nil, nil, nil, ErrUsernameAndPasswordRequired } // perform login var err error c, auth, err = m.NewClientWithLogin(ctx, username, []byte(password)) if err != nil { return nil, nil, nil, nil, nil, nil, err } c.AddAuthHandler(authHandler) c.AddDeauthHandler(deAuthHandler) if auth.TwoFA.Enabled&proton.HasTOTP != 0 { if config.FirstLoginCredential.TwoFA != "" { err := c.Auth2FA(ctx, proton.Auth2FAReq{ TwoFactorCode: config.FirstLoginCredential.TwoFA, }) if err != nil { return nil, nil, nil, nil, nil, nil, err } } else { return nil, nil, nil, nil, nil, nil, Err2FACodeRequired } } var keyPass []byte if auth.PasswordMode == proton.TwoPasswordMode { if config.FirstLoginCredential.MailboxPassword != "" { keyPass = []byte(config.FirstLoginCredential.MailboxPassword) } else { return nil, nil, nil, nil, nil, nil, ErrMailboxPasswordRequired } } else { keyPass = []byte(config.FirstLoginCredential.Password) } // decrypt keyring var saltedKeyPassByteArr []byte userKR, addrKRs, addrs, saltedKeyPassByteArr, err = getAccountKRs(ctx, c, keyPass, nil) if err != nil { return nil, nil, nil, nil, nil, nil, err } saltedKeyPass := base64.StdEncoding.EncodeToString(saltedKeyPassByteArr) config.ReusableCredential.UID = auth.UID config.ReusableCredential.AccessToken = auth.AccessToken config.ReusableCredential.RefreshToken = auth.RefreshToken config.ReusableCredential.SaltedKeyPass = saltedKeyPass err = cacheCredentialToFile(config) if err != nil { return nil, nil, nil, nil, nil, nil, err } return m, c, &ProtonDriveCredential{ UID: auth.UID, AccessToken: auth.AccessToken, RefreshToken: auth.RefreshToken, SaltedKeyPass: saltedKeyPass, }, userKR, addrKRs, addrs, nil } } func Logout(ctx context.Context, config *Config, m *proton.Manager, c *proton.Client, userKR *crypto.KeyRing, addrKRs map[string]*crypto.KeyRing) error { defer m.Close() defer c.Close() if config.CredentialCacheFile == "" { log.Println("Logging out user") // log out err := c.AuthDelete(ctx) if err != nil { return err } // clear keyrings userKR.ClearPrivateParams() for i := range addrKRs { addrKRs[i].ClearPrivateParams() } } return nil } Proton-API-Bridge-1.0.0/constants.go000066400000000000000000000010501447740121100171320ustar00rootroot00000000000000package proton_api_bridge var ( LIB_VERSION = "1.0.0" UPLOAD_BLOCK_SIZE = 4 * 1024 * 1024 // 4 MB UPLOAD_BATCH_BLOCK_SIZE = 8 /* https://github.com/rclone/rclone/pull/7093#issuecomment-1637024885 The idea is that rclone performs buffering / pre-fetching on it's own so we don't need to be doing this on our end. If you are not using rclone and instead is directly basing your work on this library, then maybe you can increase this value to let the library does the buffering work for you! */ DOWNLOAD_BATCH_BLOCK_SIZE = 1 ) Proton-API-Bridge-1.0.0/crypto.go000066400000000000000000000073471447740121100164550ustar00rootroot00000000000000package proton_api_bridge import ( "crypto/sha256" "encoding/base64" "io" "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/gopenpgp/v2/helper" ) func generatePassphrase() (string, error) { token, err := crypto.RandomToken(32) if err != nil { return "", err } tokenBase64 := base64.StdEncoding.EncodeToString(token) return tokenBase64, nil } func generateCryptoKey() (string, string, error) { passphrase, err := generatePassphrase() if err != nil { return "", "", err } // all hardcoded values from iOS drive key, err := helper.GenerateKey("Drive key", "noreply@protonmail.com", []byte(passphrase), "x25519", 0) if err != nil { return "", "", err } return passphrase, key, nil } // taken from Proton Go API Backend func encryptWithSignature(kr, addrKR *crypto.KeyRing, b []byte) (string, string, error) { enc, err := kr.Encrypt(crypto.NewPlainMessage(b), nil) if err != nil { return "", "", err } encArm, err := enc.GetArmored() if err != nil { return "", "", err } sig, err := addrKR.SignDetached(crypto.NewPlainMessage(b)) if err != nil { return "", "", err } sigArm, err := sig.GetArmored() if err != nil { return "", "", err } return encArm, sigArm, nil } func generateNodeKeys(kr, addrKR *crypto.KeyRing) (string, string, string, error) { nodePassphrase, nodeKey, err := generateCryptoKey() if err != nil { return "", "", "", err } nodePassphraseEnc, nodePassphraseSignature, err := encryptWithSignature(kr, addrKR, []byte(nodePassphrase)) if err != nil { return "", "", "", err } return nodeKey, nodePassphraseEnc, nodePassphraseSignature, nil } func reencryptKeyPacket(srcKR, dstKR, addrKR *crypto.KeyRing, passphrase string) (string, error) { oldSplitMessage, err := crypto.NewPGPSplitMessageFromArmored(passphrase) if err != nil { return "", err } sessionKey, err := srcKR.DecryptSessionKey(oldSplitMessage.KeyPacket) if err != nil { return "", err } newKeyPacket, err := dstKR.EncryptSessionKey(sessionKey) if err != nil { return "", err } newSplitMessage := crypto.NewPGPSplitMessage(newKeyPacket, oldSplitMessage.DataPacket) return newSplitMessage.GetArmored() } func getKeyRing(kr, addrKR *crypto.KeyRing, key, passphrase, passphraseSignature string) (*crypto.KeyRing, error) { enc, err := crypto.NewPGPMessageFromArmored(passphrase) if err != nil { return nil, err } dec, err := kr.Decrypt(enc, nil, crypto.GetUnixTime()) if err != nil { return nil, err } sig, err := crypto.NewPGPSignatureFromArmored(passphraseSignature) if err != nil { return nil, err } if err := addrKR.VerifyDetached(dec, sig, crypto.GetUnixTime()); err != nil { return nil, err } lockedKey, err := crypto.NewKeyFromArmored(key) if err != nil { return nil, err } unlockedKey, err := lockedKey.Unlock(dec.GetBinary()) if err != nil { return nil, err } return crypto.NewKeyRing(unlockedKey) } func decryptBlockIntoBuffer(sessionKey *crypto.SessionKey, addrKR, nodeKR *crypto.KeyRing, originalHash, encSignature string, buffer io.ReaderFrom, block io.ReadCloser) error { data, err := io.ReadAll(block) if err != nil { return err } plainMessage, err := sessionKey.Decrypt(data) if err != nil { return err } encSignatureArm, err := crypto.NewPGPMessageFromArmored(encSignature) if err != nil { return err } err = addrKR.VerifyDetachedEncrypted(plainMessage, encSignatureArm, nodeKR, crypto.GetUnixTime()) if err != nil { return err } _, err = buffer.ReadFrom(plainMessage.NewReader()) if err != nil { return err } h := sha256.New() h.Write(data) hash := h.Sum(nil) base64Hash := base64.StdEncoding.EncodeToString(hash) if err != nil { return err } if base64Hash != originalHash { return ErrDownloadedBlockHashVerificationFailed } return nil } Proton-API-Bridge-1.0.0/delete.go000066400000000000000000000056731447740121100163770ustar00rootroot00000000000000package proton_api_bridge import ( "context" "github.com/henrybear327/go-proton-api" ) func (protonDrive *ProtonDrive) moveToTrash(ctx context.Context, parentLinkID string, linkIDs ...string) error { err := protonDrive.c.TrashChildren(ctx, protonDrive.MainShare.ShareID, parentLinkID, linkIDs...) if err != nil { return err } for _, link := range linkIDs { protonDrive.removeLinkIDFromCache(link, true) } return nil } func (protonDrive *ProtonDrive) MoveFileToTrashByID(ctx context.Context, linkID string) error { /* It's like event system, we need to get the latest information before creating the move request! */ protonDrive.removeLinkIDFromCache(linkID, false) fileLink, err := protonDrive.getLink(ctx, linkID) if err != nil { return err } if fileLink.Type != proton.LinkTypeFile { return ErrLinkTypeMustToBeFileType } return protonDrive.moveToTrash(ctx, fileLink.ParentLinkID, linkID) } func (protonDrive *ProtonDrive) MoveFolderToTrashByID(ctx context.Context, linkID string, onlyOnEmpty bool) error { /* It's like event system, we need to get the latest information before creating the move request! */ protonDrive.removeLinkIDFromCache(linkID, false) folderLink, err := protonDrive.getLink(ctx, linkID) if err != nil { return err } if folderLink.Type != proton.LinkTypeFolder { return ErrLinkTypeMustToBeFolderType } childrenLinks, err := protonDrive.c.ListChildren(ctx, protonDrive.MainShare.ShareID, linkID /* false: list only active ones */, false) if err != nil { return err } if onlyOnEmpty { if len(childrenLinks) > 0 { return ErrFolderIsNotEmpty } } return protonDrive.moveToTrash(ctx, folderLink.ParentLinkID, linkID) } // WARNING!!!! // Everything in the root folder will be moved to trash // Most likely only used for debugging when the key is messed up func (protonDrive *ProtonDrive) EmptyRootFolder(ctx context.Context) error { protonDrive.ClearCache() links, err := protonDrive.c.ListChildren(ctx, protonDrive.MainShare.ShareID, protonDrive.MainShare.LinkID, true) if err != nil { return err } { linkIDs := make([]string, 0) for i := range links { if links[i].State == proton.LinkStateActive /* use TrashChildren */ { linkIDs = append(linkIDs, links[i].LinkID) } } err := protonDrive.c.TrashChildren(ctx, protonDrive.MainShare.ShareID, protonDrive.MainShare.LinkID, linkIDs...) if err != nil { return err } } { linkIDs := make([]string, 0) for i := range links { if links[i].State != proton.LinkStateActive { linkIDs = append(linkIDs, links[i].LinkID) } } err := protonDrive.c.DeleteChildren(ctx, protonDrive.MainShare.ShareID, protonDrive.MainShare.LinkID, linkIDs...) if err != nil { return err } } return nil } // Empty the trash func (protonDrive *ProtonDrive) EmptyTrash(ctx context.Context) error { protonDrive.ClearCache() err := protonDrive.c.EmptyTrash(ctx, protonDrive.MainShare.ShareID) if err != nil { return err } return nil } Proton-API-Bridge-1.0.0/drive.go000066400000000000000000000132731447740121100162410ustar00rootroot00000000000000package proton_api_bridge import ( "context" "log" "github.com/henrybear327/Proton-API-Bridge/common" "golang.org/x/sync/semaphore" "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/henrybear327/go-proton-api" ) type ProtonDrive struct { MainShare *proton.Share RootLink *proton.Link MainShareKR *crypto.KeyRing DefaultAddrKR *crypto.KeyRing Config *common.Config c *proton.Client m *proton.Manager userKR *crypto.KeyRing addrKRs map[string]*crypto.KeyRing addrData map[string]proton.Address signatureAddress string cache *cache blockUploadSemaphore *semaphore.Weighted blockCryptoSemaphore *semaphore.Weighted } func NewDefaultConfig() *common.Config { return common.NewConfigWithDefaultValues() } func NewProtonDrive(ctx context.Context, config *common.Config, authHandler proton.AuthHandler, deAuthHandler proton.Handler) (*ProtonDrive, *common.ProtonDriveCredential, error) { /* Log in and logout */ m, c, credentials, userKR, addrKRs, addrData, err := common.Login(ctx, config, authHandler, deAuthHandler) if err != nil { return nil, nil, err } /* Current understanding (at the time of the commit) The volume is the mount point. A link is like a folder in POSIX. A share is associated with a link to represent the access control, and serves as an entry point to a location in the file structure (Volume). It points to a link, of file or folder type, anywhere in the tree and holds a key called the ShareKey. To access a link, of file or folder type, a user must be a member of a share. A volume has a default share for access control and is owned by the creator of the volume. A volume has a default link as it's root folder. MIMETYPE holds type, e.g. folder, image/png, etc. */ volumes, err := listAllVolumes(ctx, c) if err != nil { return nil, nil, err } // log.Printf("all volumes %#v", volumes) mainShareID := "" for i := range volumes { // iOS drive: first active volume if volumes[i].State == proton.VolumeStateActive { mainShareID = volumes[i].Share.ShareID } } // log.Println("total volumes", len(volumes), "mainShareID", mainShareID) /* Get root folder from the main share of the volume */ mainShare, err := getShareByID(ctx, c, mainShareID) if err != nil { return nil, nil, err } // check for main share integrity { mainShareCheck := false shares, err := getAllShares(ctx, c) if err != nil { return nil, nil, err } for i := range shares { if shares[i].ShareID == mainShare.ShareID && shares[i].LinkID == mainShare.LinkID && shares[i].Flags == proton.PrimaryShare && shares[i].Type == proton.ShareTypeMain { mainShareCheck = true } } if !mainShareCheck { log.Printf("mainShare %#v", mainShare) log.Printf("shares %#v", shares) return nil, nil, ErrMainSharePreconditionsFailed } } // Note: rootLink's parentLinkID == "" /* Link holds the tree structure, for the clients, they represent the files and folders of a given volume. They have a ParentLinkID that points to parent folders. Links also hold the file name (encrypted) and a hash of the name for name collisions. Link data is encrypted with its owning Share keyring. */ rootLink, err := c.GetLink(ctx, mainShare.ShareID, mainShare.LinkID) if err != nil { return nil, nil, err } if err != nil { return nil, nil, err } // log.Printf("rootLink %#v", rootLink) // log.Printf("addrKRs %#v", addrKRs)= mainShareAddrKR := addrKRs[mainShare.AddressID] // log.Println("addrKR CountDecryptionEntities", addrKR.CountDecryptionEntities()) mainShareKR, err := mainShare.GetKeyRing(mainShareAddrKR) if err != nil { return nil, nil, err } // log.Println("mainShareKR CountDecryptionEntities", mainShareKR.CountDecryptionEntities()) return &ProtonDrive{ MainShare: mainShare, RootLink: &rootLink, MainShareKR: mainShareKR, DefaultAddrKR: mainShareAddrKR, Config: config, c: c, m: m, userKR: userKR, addrKRs: addrKRs, addrData: addrData, signatureAddress: mainShare.Creator, cache: newCache(config.EnableCaching), blockUploadSemaphore: semaphore.NewWeighted(int64(config.ConcurrentBlockUploadCount)), blockCryptoSemaphore: semaphore.NewWeighted(int64(config.ConcurrentFileCryptoCount)), }, credentials, nil } func (protonDrive *ProtonDrive) Logout(ctx context.Context) error { return common.Logout(ctx, protonDrive.Config, protonDrive.m, protonDrive.c, protonDrive.userKR, protonDrive.addrKRs) } func (protonDrive *ProtonDrive) About(ctx context.Context) (*proton.User, error) { user, err := protonDrive.c.GetUser(ctx) if err != nil { return nil, err } return &user, nil } func (protonDrive *ProtonDrive) GetLink(ctx context.Context, linkID string) (*proton.Link, error) { return protonDrive.getLink(ctx, linkID) } func addKeysFromKR(kr *crypto.KeyRing, newKRs ...*crypto.KeyRing) error { for i := range newKRs { for _, key := range newKRs[i].GetKeys() { err := kr.AddKey(key) if err != nil { return err } } } return nil } func (protonDrive *ProtonDrive) getSignatureVerificationKeyring(emailAddresses []string, verificationAddrKRs ...*crypto.KeyRing) (*crypto.KeyRing, error) { ret, err := crypto.NewKeyRing(nil) if err != nil { return nil, err } for _, emailAddress := range emailAddresses { if addr, ok := protonDrive.addrData[emailAddress]; ok { if err := addKeysFromKR(ret, protonDrive.addrKRs[addr.ID]); err != nil { return nil, err } } } if err := addKeysFromKR(ret, verificationAddrKRs...); err != nil { return nil, err } if ret.CountEntities() == 0 { return nil, ErrNoKeyringForSignatureVerification } return ret, nil } Proton-API-Bridge-1.0.0/drive_test.go000066400000000000000000000521561447740121100173030ustar00rootroot00000000000000package proton_api_bridge import ( "log" "strings" "testing" "github.com/henrybear327/go-proton-api" ) func TestCreateAndDeleteFolder(t *testing.T) { ctx, cancel, protonDrive := setup(t, false) t.Cleanup(func() { defer cancel() defer tearDown(t, ctx, protonDrive) }) log.Println("Create a folder tmp at root") createFolder(t, ctx, protonDrive, "", "tmp") checkActiveFileListing(t, ctx, protonDrive, []string{"/tmp"}) log.Println("Delete folder tmp") deleteBySearchingFromRoot(t, ctx, protonDrive, "tmp", true, false) checkActiveFileListing(t, ctx, protonDrive, []string{}) } func TestCreateAndCreateAndDeleteFolder(t *testing.T) { ctx, cancel, protonDrive := setup(t, false) t.Cleanup(func() { defer cancel() defer tearDown(t, ctx, protonDrive) }) log.Println("Create a folder tmp at root") createFolder(t, ctx, protonDrive, "", "tmp") checkActiveFileListing(t, ctx, protonDrive, []string{"/tmp"}) log.Println("Create a folder tmp at root again") createFolderExpectError(t, ctx, protonDrive, "", "tmp", proton.ErrFolderNameExist) checkActiveFileListing(t, ctx, protonDrive, []string{"/tmp"}) log.Println("Delete folder tmp") deleteBySearchingFromRoot(t, ctx, protonDrive, "tmp", true, false) } func TestUploadAndDownloadAndDeleteAFile(t *testing.T) { ctx, cancel, protonDrive := setup(t, true) t.Cleanup(func() { defer cancel() defer tearDown(t, ctx, protonDrive) }) log.Println("Upload integrationTestImage.png") uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png", 0) checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 1, 0, 0) checkActiveFileListing(t, ctx, protonDrive, []string{"/integrationTestImage.png"}) downloadFile(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png", "") log.Println("Delete file integrationTestImage.png") deleteBySearchingFromRoot(t, ctx, protonDrive, "integrationTestImage.png", false, false) checkActiveFileListing(t, ctx, protonDrive, []string{}) } func TestUploadAndUploadAndDownloadAndDeleteAFile(t *testing.T) { ctx, cancel, protonDrive := setup(t, true) t.Cleanup(func() { defer cancel() defer tearDown(t, ctx, protonDrive) }) log.Println("Upload integrationTestImage.png") uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png", 0) checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 1, 0, 0) checkActiveFileListing(t, ctx, protonDrive, []string{"/integrationTestImage.png"}) downloadFile(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png", "") log.Println("Upload a new revision to replace integrationTestImage.png") uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage2.png", 0) /* Add a revision */ checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 2, 1, 0, 1) downloadFile(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage2.png", "") log.Println("Delete file integrationTestImage.png") deleteBySearchingFromRoot(t, ctx, protonDrive, "integrationTestImage.png", false, false) checkActiveFileListing(t, ctx, protonDrive, []string{}) } func TestPartialUploadAndReuploadFailedAndDownloadAndDeleteAFile(t *testing.T) { ctx, cancel, protonDrive := setup(t, false) t.Cleanup(func() { defer cancel() defer tearDown(t, ctx, protonDrive) }) log.Println("Create a new draft revision of integrationTestImage.png") uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png", 1) checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 0, 1, 0) checkActiveFileListing(t, ctx, protonDrive, []string{}) log.Println("Create a new draft revision of integrationTestImage.png again") uploadFileByFilepathWithError(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png", 1, ErrDraftExists) checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 0, 1, 0) checkActiveFileListing(t, ctx, protonDrive, []string{}) // FIXME: delete file with draft revision only // log.Println("Delete file integrationTestImage.png") // deleteBySearchingFromRoot(t, ctx, protonDrive, "integrationTestImage.png", false, true) // checkActiveFileListing(t, ctx, protonDrive, []string{}) } func TestPartialUploadAndReuploadAndDownloadAndDeleteAFile(t *testing.T) { ctx, cancel, protonDrive := setup(t, true) t.Cleanup(func() { defer cancel() defer tearDown(t, ctx, protonDrive) }) log.Println("Create a new draft revision of integrationTestImage.png") uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png", 1) checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 0, 1, 0) checkActiveFileListing(t, ctx, protonDrive, []string{}) log.Println("Create a new draft revision of integrationTestImage.png again") uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png", 1) checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 0, 1, 0) checkActiveFileListing(t, ctx, protonDrive, []string{}) log.Println("Create a new revision and don't commit integrationTestImage.png") uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage2.png", 2) checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 0, 1, 0) checkActiveFileListing(t, ctx, protonDrive, []string{}) log.Println("Create a new revision and commit it to replace integrationTestImage.png") uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage2.png", 0) /* Add a revision */ checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 1, 0, 0) downloadFile(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage2.png", "") checkActiveFileListing(t, ctx, protonDrive, []string{"/integrationTestImage.png"}) log.Println("Delete file integrationTestImage.png") deleteBySearchingFromRoot(t, ctx, protonDrive, "integrationTestImage.png", false, false) checkActiveFileListing(t, ctx, protonDrive, []string{}) } func TestUploadAndDownloadThreeRevisionsAndDeleteAFile(t *testing.T) { ctx, cancel, protonDrive := setup(t, true) t.Cleanup(func() { defer cancel() defer tearDown(t, ctx, protonDrive) }) log.Println("Create a new revision and commit it to replace integrationTestImage.png") uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png", 0) /* Add a revision */ checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 1, 0, 0) downloadFile(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png", "") checkActiveFileListing(t, ctx, protonDrive, []string{"/integrationTestImage.png"}) log.Println("Create a new revision 2 and commit it to replace integrationTestImage.png") uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/empty.txt", 0) /* Add a revision */ checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 2, 1, 0, 1) downloadFile(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/empty.txt", "") checkActiveFileListing(t, ctx, protonDrive, []string{"/integrationTestImage.png"}) log.Println("Create a new revision 3 and commit it to replace integrationTestImage.png") uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage2.png", 0) /* Add a revision */ checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 3, 1, 0, 2) downloadFile(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage2.png", "") checkActiveFileListing(t, ctx, protonDrive, []string{"/integrationTestImage.png"}) log.Println("Delete file integrationTestImage.png") deleteBySearchingFromRoot(t, ctx, protonDrive, "integrationTestImage.png", false, false) checkActiveFileListing(t, ctx, protonDrive, []string{}) } func TestPartialUploadAndReuploadAndDownloadTwiceAndDeleteAFileSmallBig(t *testing.T) { ctx, cancel, protonDrive := setup(t, true) t.Cleanup(func() { defer cancel() defer tearDown(t, ctx, protonDrive) }) log.Println("Create a new draft revision of integrationTestImage.png") uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/empty.txt", 1) checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 0, 1, 0) checkActiveFileListing(t, ctx, protonDrive, []string{}) log.Println("Create a new draft revision of integrationTestImage.png again") uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/empty.txt", 1) checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 0, 1, 0) checkActiveFileListing(t, ctx, protonDrive, []string{}) log.Println("Create a new revision and don't commit integrationTestImage.png") uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/empty.txt", 2) checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 0, 1, 0) checkActiveFileListing(t, ctx, protonDrive, []string{}) log.Println("Create a new revision and commit it to replace integrationTestImage.png") uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/empty.txt", 0) /* Add a revision */ checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 1, 0, 0) downloadFile(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/empty.txt", "") checkActiveFileListing(t, ctx, protonDrive, []string{"/integrationTestImage.png"}) log.Println("Create a new revision 2 and don't commit integrationTestImage.png") uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/empty.txt", 2) checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 2, 1, 1, 0) checkActiveFileListing(t, ctx, protonDrive, []string{"/integrationTestImage.png"}) // testing the reduce new revision size log.Println("Create a new revision 2 and commit it to replace integrationTestImage.png") uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png", 0) /* Add a revision */ checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 2, 1, 0, 1) downloadFile(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png", "") checkActiveFileListing(t, ctx, protonDrive, []string{"/integrationTestImage.png"}) log.Println("Delete file integrationTestImage.png") deleteBySearchingFromRoot(t, ctx, protonDrive, "integrationTestImage.png", false, false) checkActiveFileListing(t, ctx, protonDrive, []string{}) } func TestUploadAndDeleteAnEmptyFileAtRoot(t *testing.T) { ctx, cancel, protonDrive := setup(t, false) t.Cleanup(func() { defer cancel() defer tearDown(t, ctx, protonDrive) }) log.Println("Upload empty.txt") uploadFileByFilepath(t, ctx, protonDrive, "", "empty.txt", "testcase/empty.txt", 0) checkRevisions(protonDrive, ctx, t, "empty.txt", 1, 1, 0, 0) checkActiveFileListing(t, ctx, protonDrive, []string{"/empty.txt"}) downloadFile(t, ctx, protonDrive, "", "empty.txt", "testcase/empty.txt", "") log.Println("Upload a new revision to replace empty.txt") uploadFileByFilepath(t, ctx, protonDrive, "", "empty.txt", "testcase/empty.txt", 0) /* Add a revision */ checkRevisions(protonDrive, ctx, t, "empty.txt", 2, 1, 0, 1) downloadFile(t, ctx, protonDrive, "", "empty.txt", "testcase/empty.txt", "") checkActiveFileListing(t, ctx, protonDrive, []string{"/empty.txt"}) log.Println("Delete file empty.txt") deleteBySearchingFromRoot(t, ctx, protonDrive, "empty.txt", false, false) checkActiveFileListing(t, ctx, protonDrive, []string{}) } func TestUploadAndDownloadAndDeleteAFileAtAFolderOneLevelFromRoot(t *testing.T) { ctx, cancel, protonDrive := setup(t, false) t.Cleanup(func() { defer cancel() defer tearDown(t, ctx, protonDrive) }) log.Println("Create folder level1") createFolder(t, ctx, protonDrive, "", "level1") checkActiveFileListing(t, ctx, protonDrive, []string{"/level1"}) log.Println("Upload integrationTestImage.png to level1") uploadFileByFilepath(t, ctx, protonDrive, "level1", "integrationTestImage.png", "testcase/integrationTestImage.png", 0) checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 1, 0, 0) checkActiveFileListing(t, ctx, protonDrive, []string{"/level1", "/level1/integrationTestImage.png"}) downloadFile(t, ctx, protonDrive, "level1", "integrationTestImage.png", "testcase/integrationTestImage.png", "") log.Println("Upload a new revision to replace integrationTestImage.png in level1") uploadFileByFilepath(t, ctx, protonDrive, "level1", "integrationTestImage.png", "testcase/integrationTestImage2.png", 0) /* Add a revision */ checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 2, 1, 0, 1) downloadFile(t, ctx, protonDrive, "level1", "integrationTestImage.png", "testcase/integrationTestImage2.png", "") log.Println("Delete folder level1") deleteBySearchingFromRoot(t, ctx, protonDrive, "level1", true, false) checkActiveFileListing(t, ctx, protonDrive, []string{}) } func TestCreateAndMoveAndDeleteFolder(t *testing.T) { ctx, cancel, protonDrive := setup(t, false) t.Cleanup(func() { defer cancel() defer tearDown(t, ctx, protonDrive) }) log.Println("Create a folder src at root") createFolder(t, ctx, protonDrive, "", "src") checkActiveFileListing(t, ctx, protonDrive, []string{"/src"}) log.Println("Create a folder dst at root") createFolder(t, ctx, protonDrive, "", "dst") checkActiveFileListing(t, ctx, protonDrive, []string{"/src", "/dst"}) log.Println("Move folder src to under folder dst") moveFolder(t, ctx, protonDrive, "src", "dst") checkActiveFileListing(t, ctx, protonDrive, []string{"/dst", "/dst/src"}) log.Println("Delete folder dst") deleteBySearchingFromRoot(t, ctx, protonDrive, "dst", true, false) checkActiveFileListing(t, ctx, protonDrive, []string{}) } func TestCreateAndMoveAndDeleteFolderWithAFile(t *testing.T) { ctx, cancel, protonDrive := setup(t, false) t.Cleanup(func() { defer cancel() defer tearDown(t, ctx, protonDrive) }) log.Println("Create a folder src at root") createFolder(t, ctx, protonDrive, "", "src") checkActiveFileListing(t, ctx, protonDrive, []string{"/src"}) log.Println("Upload integrationTestImage.png to src") uploadFileByFilepath(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage.png", 0) checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 1, 0, 0) checkActiveFileListing(t, ctx, protonDrive, []string{"/src", "/src/integrationTestImage.png"}) downloadFile(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage.png", "") log.Println("Create a folder dst at root") createFolder(t, ctx, protonDrive, "", "dst") checkActiveFileListing(t, ctx, protonDrive, []string{"/src", "/src/integrationTestImage.png", "/dst"}) log.Println("Upload a new revision to replace integrationTestImage.png in src") uploadFileByFilepath(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage2.png", 0) /* Add a revision */ checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 2, 1, 0, 1) downloadFile(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage2.png", "") checkActiveFileListing(t, ctx, protonDrive, []string{"/src", "/src/integrationTestImage.png", "/dst"}) log.Println("Move folder src to under folder dst") moveFolder(t, ctx, protonDrive, "src", "dst") checkActiveFileListing(t, ctx, protonDrive, []string{"/dst", "/dst/src", "/dst/src/integrationTestImage.png"}) log.Println("Delete folder dst") deleteBySearchingFromRoot(t, ctx, protonDrive, "dst", true, false) checkActiveFileListing(t, ctx, protonDrive, []string{}) } func TestCreateAndMoveAndDeleteAFileOneLevelFromRoot(t *testing.T) { ctx, cancel, protonDrive := setup(t, false) t.Cleanup(func() { defer cancel() defer tearDown(t, ctx, protonDrive) }) log.Println("Create a folder src at root") createFolder(t, ctx, protonDrive, "", "src") checkActiveFileListing(t, ctx, protonDrive, []string{"/src"}) log.Println("Upload integrationTestImage.png to src") uploadFileByFilepath(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage.png", 0) checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 1, 0, 0) checkActiveFileListing(t, ctx, protonDrive, []string{"/src", "/src/integrationTestImage.png"}) downloadFile(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage.png", "") log.Println("Create a folder dst at root") createFolder(t, ctx, protonDrive, "", "dst") checkActiveFileListing(t, ctx, protonDrive, []string{"/src", "/src/integrationTestImage.png", "/dst"}) log.Println("Upload a new revision to replace integrationTestImage.png in src") uploadFileByFilepath(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage2.png", 0) /* Add a revision */ checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 2, 1, 0, 1) downloadFile(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage2.png", "") checkActiveFileListing(t, ctx, protonDrive, []string{"/src", "/src/integrationTestImage.png", "/dst"}) log.Println("Move file integrationTestImage.png to under folder dst") moveFile(t, ctx, protonDrive, "integrationTestImage.png", "dst") checkActiveFileListing(t, ctx, protonDrive, []string{"/src", "/dst", "/dst/integrationTestImage.png"}) log.Println("Delete folder dst") deleteBySearchingFromRoot(t, ctx, protonDrive, "dst", true, false) checkActiveFileListing(t, ctx, protonDrive, []string{"/src"}) log.Println("Delete folder src") deleteBySearchingFromRoot(t, ctx, protonDrive, "src", true, false) checkActiveFileListing(t, ctx, protonDrive, []string{}) } func TestUploadLargeNumberOfBlocks(t *testing.T) { ctx, cancel, protonDrive := setup(t, false) t.Cleanup(func() { defer cancel() defer tearDown(t, ctx, protonDrive) }) // in order to simulate uploading large files // we use 1KB for the UPLOAD_BLOCK_SIZE // so a 1000KB file will generate 1000 blocks to test the uploading mechanism // and also testing the downloading mechanism ORIGINAL_UPLOAD_BLOCK_SIZE := UPLOAD_BLOCK_SIZE defer func() { UPLOAD_BLOCK_SIZE = ORIGINAL_UPLOAD_BLOCK_SIZE }() blocks := 100 UPLOAD_BLOCK_SIZE = 10 filename := "fileContent.txt" file1Content := RandomString(UPLOAD_BLOCK_SIZE*blocks + 1) // intentionally make the data not aligned to a block file1ContentReader := strings.NewReader(file1Content) file2Content := RandomString(UPLOAD_BLOCK_SIZE*blocks + 5) file2ContentReader := strings.NewReader(file2Content) log.Println("Upload fileContent.txt") uploadFileByReader(t, ctx, protonDrive, "", filename, file1ContentReader, 0) checkRevisions(protonDrive, ctx, t, filename, 1, 1, 0, 0) checkActiveFileListing(t, ctx, protonDrive, []string{"/" + filename}) downloadFile(t, ctx, protonDrive, "", filename, "", file1Content) log.Println("Upload a new revision to replace fileContent.txt") uploadFileByReader(t, ctx, protonDrive, "", filename, file2ContentReader, 0) checkRevisions(protonDrive, ctx, t, filename, 2, 1, 0, 1) checkActiveFileListing(t, ctx, protonDrive, []string{"/" + filename}) downloadFile(t, ctx, protonDrive, "", filename, "", file2Content) log.Println("Delete file fileContent.txt") deleteBySearchingFromRoot(t, ctx, protonDrive, filename, false, false) checkActiveFileListing(t, ctx, protonDrive, []string{}) } func TestFileSeek(t *testing.T) { ctx, cancel, protonDrive := setup(t, false) t.Cleanup(func() { defer cancel() defer tearDown(t, ctx, protonDrive) }) // in order to simulate seeking over blocks // we use 1KB for the UPLOAD_BLOCK_SIZE ORIGINAL_UPLOAD_BLOCK_SIZE := UPLOAD_BLOCK_SIZE defer func() { UPLOAD_BLOCK_SIZE = ORIGINAL_UPLOAD_BLOCK_SIZE }() blocks := 10 UPLOAD_BLOCK_SIZE = 10 filename := "fileContent.txt" file1Content := RandomString(UPLOAD_BLOCK_SIZE*blocks + 5) // intentionally make the data not aligned to a block file1ContentReader := strings.NewReader(file1Content) log.Println("Upload fileContent.txt") uploadFileByReader(t, ctx, protonDrive, "", filename, file1ContentReader, 0) checkRevisions(protonDrive, ctx, t, filename, 1, 1, 0, 0) checkActiveFileListing(t, ctx, protonDrive, []string{"/" + filename}) { log.Println("Download fileContent.txt with offset 0") downloadFileWithOffset(t, ctx, protonDrive, "", filename, "", file1Content, 0) } { offset := int64(UPLOAD_BLOCK_SIZE) log.Println("Download fileContent.txt with offset", offset) downloadFileWithOffset(t, ctx, protonDrive, "", filename, "", file1Content[offset:], offset) } { offset := int64(UPLOAD_BLOCK_SIZE + 5) log.Println("Download fileContent.txt with offset", offset) downloadFileWithOffset(t, ctx, protonDrive, "", filename, "", file1Content[offset:], offset) } { offset := int64(UPLOAD_BLOCK_SIZE*blocks/2 + 3) log.Println("Download fileContent.txt with offset", offset) downloadFileWithOffset(t, ctx, protonDrive, "", filename, "", file1Content[offset:], offset) } log.Println("Delete file fileContent.txt") deleteBySearchingFromRoot(t, ctx, protonDrive, filename, false, false) checkActiveFileListing(t, ctx, protonDrive, []string{}) } Proton-API-Bridge-1.0.0/drive_test_helper.go000066400000000000000000000271701447740121100206400ustar00rootroot00000000000000package proton_api_bridge import ( "bufio" "bytes" "context" "io" "os" "testing" "time" "github.com/henrybear327/Proton-API-Bridge/common" "github.com/henrybear327/Proton-API-Bridge/utility" "github.com/henrybear327/go-proton-api" mathrand "math/rand" ) func setup(t *testing.T, replaceExistingDraft bool) (context.Context, context.CancelFunc, *ProtonDrive) { utility.SetupLog() config := common.NewConfigForIntegrationTests() config.ReplaceExistingDraft = replaceExistingDraft { // pre-condition check if !config.DestructiveIntegrationTest { t.Fatalf("CAUTION: the integration test requires a clean proton drive") } if !config.EmptyTrashAfterIntegrationTest { t.Fatalf("CAUTION: the integration test requires cleaning up the drive after running the tests") } } ctx, cancel := context.WithCancel(context.Background()) protonDrive, auth, err := NewProtonDrive(ctx, config, func(auth proton.Auth) {}, func() {}) if err != nil { t.Fatal(err) } err = protonDrive.EmptyRootFolder(ctx) if err != nil { t.Fatal(err) } if config.UseReusableLogin { if auth != nil { t.Fatalf("Auth should be nil") } } else { if auth == nil { t.Fatalf("Auth should not be nil") } } err = protonDrive.EmptyTrash(ctx) if err != nil { t.Fatal(err) } return ctx, cancel, protonDrive } func tearDown(t *testing.T, ctx context.Context, protonDrive *ProtonDrive) { if protonDrive.Config.EmptyTrashAfterIntegrationTest { err := protonDrive.EmptyTrash(ctx) if err != nil { t.Fatal(err) } } } // Taken from: https://github.com/rclone/rclone/blob/e43b5ce5e59b5717a9819ff81805dd431f710c10/lib/random/random.go // // StringFn create a random string for test purposes using the random // number generator function passed in. // // Do not use these for passwords. func StringFn(n int, randIntn func(n int) int) string { const ( vowel = "aeiou" consonant = "bcdfghjklmnpqrstvwxyz" digit = "0123456789" ) pattern := []string{consonant, vowel, consonant, vowel, consonant, vowel, consonant, digit} out := make([]byte, n) p := 0 for i := range out { source := pattern[p] p = (p + 1) % len(pattern) out[i] = source[randIntn(len(source))] } return string(out) } // String create a random string for test purposes. // // Do not use these for passwords. func RandomString(n int) string { return StringFn(n, mathrand.Intn) } func createFolderExpectError(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, parent, name string, expectedError error) { parentLink := protonDrive.RootLink if parent != "" { targetFolderLink, err := protonDrive.searchByNameRecursivelyFromRoot(ctx, parent, true, false) if err != nil { t.Fatal(err) } if targetFolderLink == nil { t.Fatalf("Folder %v not found", parent) } parentLink = targetFolderLink } if parentLink.Type != proton.LinkTypeFolder { t.Fatalf("parentLink is not of folder type") } _, err := protonDrive.CreateNewFolderByID(ctx, parentLink.LinkID, name) if err != expectedError { t.Fatal(err) } } func createFolder(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, parent, name string) { createFolderExpectError(t, ctx, protonDrive, parent, name, nil) } func uploadFileByReader(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, parent, name string, in io.Reader, testParam int) { parentLink := protonDrive.RootLink if parent != "" { targetFolderLink, err := protonDrive.searchByNameRecursivelyFromRoot(ctx, parent, true, false) if err != nil { t.Fatal(err) } if targetFolderLink == nil { t.Fatalf("Folder %v not found", parent) } parentLink = targetFolderLink } if parentLink.Type != proton.LinkTypeFolder { t.Fatalf("parentLink is not of folder type") } _, _, err := protonDrive.UploadFileByReader(ctx, parentLink.LinkID, name, time.Now(), in, testParam) if err != nil { t.Fatal(err) } } func uploadFileByFilepathWithError(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, parent, name string, filepath string, testParam int, expectedError error) { parentLink := protonDrive.RootLink if parent != "" { targetFolderLink, err := protonDrive.searchByNameRecursivelyFromRoot(ctx, parent, true, false) if err != nil { t.Fatal(err) } if targetFolderLink == nil { t.Fatalf("Folder %v not found", parent) } parentLink = targetFolderLink } if parentLink.Type != proton.LinkTypeFolder { t.Fatalf("parentLink is not of folder type") } f, err := os.Open(filepath) if err != nil { t.Fatal(err) } defer f.Close() info, err := os.Stat(filepath) if err != nil { t.Fatal(err) } in := bufio.NewReader(f) _, _, err = protonDrive.UploadFileByReader(ctx, parentLink.LinkID, name, info.ModTime(), in, testParam) if err != expectedError { t.Fatal(err) } } func uploadFileByFilepath(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, parent, name string, filepath string, testParam int) { uploadFileByFilepathWithError(t, ctx, protonDrive, parent, name, filepath, testParam, nil) } func downloadFile(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, parent, name string, filepath string, data string) { downloadFileWithOffset(t, ctx, protonDrive, parent, name, filepath, data, 0) } func downloadFileWithOffset(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, parent, name string, filepath string, data string, offset int64) { parentLink := protonDrive.RootLink if parent != "" { targetFolderLink, err := protonDrive.searchByNameRecursivelyFromRoot(ctx, parent, true, false) if err != nil { t.Fatal(err) } if targetFolderLink == nil { t.Fatalf("Folder %v not found", parent) } parentLink = targetFolderLink } if parentLink.Type != proton.LinkTypeFolder { t.Fatalf("parentLink is not of folder type") } targetFileLink, err := protonDrive.searchByNameRecursivelyFromRoot(ctx, name, false, false) if err != nil { t.Fatal(err) } if targetFileLink == nil { t.Fatalf("File %v not found", name) } else { reader, sizeOnServer, fileSystemAttr, err := protonDrive.DownloadFileByID(ctx, targetFileLink.LinkID, offset) if err != nil { t.Fatal(err) } downloadedData, err := io.ReadAll(reader) if err != nil { t.Fatal(err) } /* Check file metadata */ if fileSystemAttr == nil { t.Fatalf("FileSystemAttr should not be nil") } else { if fileSystemAttr.Size != 0 && sizeOnServer == fileSystemAttr.Size { t.Fatalf("Not possible due to encryption file overhead") } if offset == 0 && len(downloadedData) != int(fileSystemAttr.Size) { t.Fatalf("Downloaded file size != uploaded file size: %#v vs %#v", len(downloadedData), int(fileSystemAttr.Size)) } } if filepath != "" { originalData, err := os.ReadFile(filepath) if err != nil { t.Fatal(err) } originalData = originalData[offset:] if !bytes.Equal(downloadedData, originalData) { t.Fatalf("Downloaded content is different from the original content") } } else if data != "" { if !bytes.Equal(downloadedData, []byte(data)) { t.Fatalf("Downloaded content is different from the original content") } } else { t.Fatalf("Nothing to verify against") } } } func checkRevisions(protonDrive *ProtonDrive, ctx context.Context, t *testing.T, name string, totalRevisions, activeRevisions, draftRevisions, obseleteRevisions int) { targetFileLink, err := protonDrive.searchByNameRecursivelyFromRoot(ctx, name, false, true) if err != nil { t.Fatal(err) } if targetFileLink == nil { t.Fatalf("File %v not found", name) } else { revisions, err := protonDrive.c.ListRevisions(ctx, protonDrive.MainShare.ShareID, targetFileLink.LinkID) if err != nil { t.Fatal(err) } if len(revisions) != totalRevisions { t.Fatalf("Missing revision") } for i := range revisions { if revisions[i].State == proton.RevisionStateActive { activeRevisions-- } if revisions[i].State == proton.RevisionStateDraft { draftRevisions-- } if revisions[i].State == proton.RevisionStateObsolete { obseleteRevisions-- } } if activeRevisions != 0 || draftRevisions != 0 || obseleteRevisions != 0 { t.Fatalf("Wrong revision count (should be all 0 here) %v %v %v", activeRevisions, draftRevisions, obseleteRevisions) } } } // During the integration test, the name much be unique since the link is returned by recursively search for the name from root func deleteBySearchingFromRoot(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, name string, isFolder bool, listAllActiveOrDraftFiles bool) { targetLink, err := protonDrive.searchByNameRecursivelyFromRoot(ctx, name, isFolder, listAllActiveOrDraftFiles) if err != nil { t.Fatal(err) } if targetLink == nil { t.Fatalf("Target %v to be deleted not found", name) } else { if isFolder { err = protonDrive.MoveFolderToTrashByID(ctx, targetLink.LinkID, false) if err != nil { t.Fatal(err) } } else { err = protonDrive.MoveFileToTrashByID(ctx, targetLink.LinkID) if err != nil { t.Fatal(err) } } } } func checkActiveFileListing(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, expectedPaths []string) { { paths := make([]string, 0) err := protonDrive.listDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, true, "", &paths) if err != nil { t.Fatal(err) } if len(paths) != len(expectedPaths) { t.Fatalf("Total path returned is differs from expected\nReturned %#v\nExpected: %#v\n", paths, expectedPaths) } for i := range paths { if paths[i] != expectedPaths[i] { t.Fatalf("The path returned is differs from the path expected\nReturned %#v\nExpected: %#v\n", paths, expectedPaths) } } } { paths := make([]string, 0) err := protonDrive.listDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, false, "", &paths) if err != nil { t.Fatal(err) } // transform newExpectedPath := make([]string, 0) newExpectedPath = append(newExpectedPath, "/root") for i := range expectedPaths { newExpectedPath = append(newExpectedPath, "/root"+expectedPaths[i]) } if len(paths) != len(newExpectedPath) { t.Fatalf("Total path returned is differs from expected\nReturned %#v\nExpected: %#v\n", paths, newExpectedPath) } for i := range paths { if paths[i] != newExpectedPath[i] { t.Fatalf("The path returned is differs from the path expected\nReturned %#v\nExpected: %#v\n", paths, newExpectedPath) } } } } func moveFolder(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, srcFolderName, dstParentFolderName string) { targetSrcFolderLink, err := protonDrive.searchByNameRecursivelyFromRoot(ctx, srcFolderName, true, false) if err != nil { t.Fatal(err) } targetDestFolderLink, err := protonDrive.searchByNameRecursivelyFromRoot(ctx, dstParentFolderName, true, false) if err != nil { t.Fatal(err) } if targetSrcFolderLink == nil || targetDestFolderLink == nil { t.Fatalf("Folder %s or %s found", srcFolderName, dstParentFolderName) } else { err := protonDrive.MoveFolder(ctx, targetSrcFolderLink, targetDestFolderLink, srcFolderName) if err != nil { t.Fatal(err) } } } func moveFile(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, srcFileName, dstParentFolderName string) { targetSrcFileLink, err := protonDrive.searchByNameRecursivelyFromRoot(ctx, srcFileName, false, false) if err != nil { t.Fatal(err) } targetDestFolderLink, err := protonDrive.searchByNameRecursivelyFromRoot(ctx, dstParentFolderName, true, false) if err != nil { t.Fatal(err) } if targetSrcFileLink == nil || targetDestFolderLink == nil { t.Fatalf("File %s or folder %s found", srcFileName, dstParentFolderName) } else { err := protonDrive.MoveFile(ctx, targetSrcFileLink, targetDestFolderLink, srcFileName) if err != nil { t.Fatal(err) } } } Proton-API-Bridge-1.0.0/error.go000066400000000000000000000041431447740121100162550ustar00rootroot00000000000000package proton_api_bridge import "errors" var ( ErrMainSharePreconditionsFailed = errors.New("the main share assumption has failed") ErrDataFolderNameIsEmpty = errors.New("please supply a DataFolderName to enabling file downloading") ErrLinkTypeMustToBeFolderType = errors.New("the link type must be of folder type") ErrLinkTypeMustToBeFileType = errors.New("the link type must be of file type") ErrFolderIsNotEmpty = errors.New("folder can't be deleted because it is not empty") ErrCantLocateRevision = errors.New("can't create a new file upload request and can't find an active/draft revision") ErrInternalErrorOnFileUpload = errors.New("either link or file creation request should be not nil") ErrMissingInputUploadAndCollectBlockData = errors.New("missing either session key or key ring") ErrLinkMustNotBeNil = errors.New("missing input proton link") ErrLinkMustBeActive = errors.New("can not operate on link state other than active") ErrDownloadedBlockHashVerificationFailed = errors.New("the hash of the downloaded block doesn't match the original hash") ErrDraftExists = errors.New("a draft exist - usually this means a file is being uploaded at another client, or, there was a failed upload attempt. Can use --protondrive-replace-existing-draft=true to temporarily override the existing draft") ErrCantFindActiveRevision = errors.New("can't find an active revision") ErrCantFindDraftRevision = errors.New("can't find a draft revision") ErrWrongUsageOfGetLinkKR = errors.New("internal error for GetLinkKR - nil passed in for link") ErrWrongUsageOfGetLink = errors.New("internal error for getLink - empty linkID passed in") ErrSeekOffsetAfterSkippingBlocks = errors.New("internal error for download seek - the offset after skipping blocks is wrong") ErrNoKeyringForSignatureVerification = errors.New(("internal error for signature verification - no keyring is generated")) ) Proton-API-Bridge-1.0.0/file.go000066400000000000000000000076061447740121100160520ustar00rootroot00000000000000package proton_api_bridge import ( "context" "time" "github.com/henrybear327/go-proton-api" "github.com/relvacode/iso8601" ) type FileSystemAttrs struct { ModificationTime time.Time Size int64 BlockSizes []int64 Digests string // sha1 string } func (protonDrive *ProtonDrive) GetRevisions(ctx context.Context, link *proton.Link, revisionType proton.RevisionState) ([]*proton.RevisionMetadata, error) { revisions, err := protonDrive.c.ListRevisions(ctx, protonDrive.MainShare.ShareID, link.LinkID) if err != nil { return nil, err } ret := make([]*proton.RevisionMetadata, 0) // Revisions are only for files, they represent “versions” of files. // Each file can have 1 active/draft revision and n obsolete revisions. for i := range revisions { if revisions[i].State == revisionType { ret = append(ret, &revisions[i]) } } return ret, nil } func (protonDrive *ProtonDrive) GetActiveRevisionAttrsByID(ctx context.Context, linkID string) (*FileSystemAttrs, error) { link, err := protonDrive.getLink(ctx, linkID) if err != nil { return nil, err } return protonDrive.GetActiveRevisionAttrs(ctx, link) } // Might return nil when xattr is missing func (protonDrive *ProtonDrive) GetActiveRevisionAttrs(ctx context.Context, link *proton.Link) (*FileSystemAttrs, error) { if link == nil { return nil, ErrLinkMustNotBeNil } revisionsMetadata, err := protonDrive.GetRevisions(ctx, link, proton.RevisionStateActive) if err != nil { return nil, err } if len(revisionsMetadata) != 1 { return nil, ErrCantFindActiveRevision } nodeKR, err := protonDrive.getLinkKR(ctx, link) if err != nil { return nil, err } signatureVerificationKR, err := protonDrive.getSignatureVerificationKeyring([]string{link.FileProperties.ActiveRevision.SignatureEmail}) if err != nil { return nil, err } revisionXAttrCommon, err := revisionsMetadata[0].GetDecXAttrString(signatureVerificationKR, nodeKR) if err != nil { return nil, err } if revisionXAttrCommon == nil { return nil, nil } modificationTime, err := iso8601.ParseString(revisionXAttrCommon.ModificationTime) if err != nil { return nil, err } var sha1Hash string if val, ok := revisionXAttrCommon.Digests["SHA1"]; ok { sha1Hash = val } else { sha1Hash = "" } return &FileSystemAttrs{ ModificationTime: modificationTime, Size: revisionXAttrCommon.Size, BlockSizes: revisionXAttrCommon.BlockSizes, Digests: sha1Hash, }, nil } func (protonDrive *ProtonDrive) GetActiveRevisionWithAttrs(ctx context.Context, link *proton.Link) (*proton.Revision, *FileSystemAttrs, error) { if link == nil { return nil, nil, ErrLinkMustNotBeNil } revisionsMetadata, err := protonDrive.GetRevisions(ctx, link, proton.RevisionStateActive) if err != nil { return nil, nil, err } if len(revisionsMetadata) != 1 { return nil, nil, ErrCantFindActiveRevision } revision, err := protonDrive.c.GetRevisionAllBlocks(ctx, protonDrive.MainShare.ShareID, link.LinkID, revisionsMetadata[0].ID) if err != nil { return nil, nil, err } nodeKR, err := protonDrive.getLinkKR(ctx, link) if err != nil { return nil, nil, err } signatureVerificationKR, err := protonDrive.getSignatureVerificationKeyring([]string{link.FileProperties.ActiveRevision.SignatureEmail}) if err != nil { return nil, nil, err } revisionXAttrCommon, err := revision.GetDecXAttrString(signatureVerificationKR, nodeKR) if err != nil { return nil, nil, err } modificationTime, err := iso8601.ParseString(revisionXAttrCommon.ModificationTime) if err != nil { return nil, nil, err } var sha1Hash string if val, ok := revisionXAttrCommon.Digests["SHA1"]; ok { sha1Hash = val } else { sha1Hash = "" } return &revision, &FileSystemAttrs{ ModificationTime: modificationTime, Size: revisionXAttrCommon.Size, BlockSizes: revisionXAttrCommon.BlockSizes, Digests: sha1Hash, }, nil } Proton-API-Bridge-1.0.0/file_download.go000066400000000000000000000111541447740121100177320ustar00rootroot00000000000000package proton_api_bridge import ( "bytes" "context" "io" "log" "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/henrybear327/go-proton-api" ) type FileDownloadReader struct { protonDrive *ProtonDrive ctx context.Context link *proton.Link data *bytes.Buffer nodeKR *crypto.KeyRing sessionKey *crypto.SessionKey revision *proton.Revision nextRevision int isEOF bool // TODO: integrity check if the entire file is read } func (r *FileDownloadReader) Read(p []byte) (int, error) { if r.data.Len() == 0 { // TODO: do we have memory sharing bug? // to avoid sharing the underlying buffer array across re-population r.data = bytes.NewBuffer(nil) // we download and decrypt more content err := r.populateBufferOnRead() if err != nil { return 0, err } if r.isEOF { // if the file has been downloaded entirely, we return EOF return 0, io.EOF } } return r.data.Read(p) } func (r *FileDownloadReader) Close() error { r.protonDrive = nil return nil } func (reader *FileDownloadReader) populateBufferOnRead() error { if len(reader.revision.Blocks) == 0 || len(reader.revision.Blocks) == reader.nextRevision { reader.isEOF = true return nil } offset := reader.nextRevision for i := offset; i-offset < DOWNLOAD_BATCH_BLOCK_SIZE && i < len(reader.revision.Blocks); i++ { // TODO: parallel download blockReader, err := reader.protonDrive.c.GetBlock(reader.ctx, reader.revision.Blocks[i].BareURL, reader.revision.Blocks[i].Token) if err != nil { return err } defer blockReader.Close() signatureVerificationKR, err := reader.protonDrive.getSignatureVerificationKeyring([]string{reader.link.SignatureEmail}, reader.nodeKR) if err != nil { return err } err = decryptBlockIntoBuffer(reader.sessionKey, signatureVerificationKR, reader.nodeKR, reader.revision.Blocks[i].Hash, reader.revision.Blocks[i].EncSignature, reader.data, blockReader) if err != nil { return err } reader.nextRevision = i + 1 } return nil } func (protonDrive *ProtonDrive) DownloadFileByID(ctx context.Context, linkID string, offset int64) (io.ReadCloser, int64, *FileSystemAttrs, error) { /* It's like event system, we need to get the latest information before creating the move request! */ protonDrive.removeLinkIDFromCache(linkID, false) link, err := protonDrive.getLink(ctx, linkID) if err != nil { return nil, 0, nil, err } return protonDrive.DownloadFile(ctx, link, offset) } func (protonDrive *ProtonDrive) DownloadFile(ctx context.Context, link *proton.Link, offset int64) (io.ReadCloser, int64, *FileSystemAttrs, error) { if link.Type != proton.LinkTypeFile { return nil, 0, nil, ErrLinkTypeMustToBeFileType } parentNodeKR, err := protonDrive.getLinkKRByID(ctx, link.ParentLinkID) if err != nil { return nil, 0, nil, err } signatureVerificationKR, err := protonDrive.getSignatureVerificationKeyring([]string{link.SignatureEmail}) if err != nil { return nil, 0, nil, err } nodeKR, err := link.GetKeyRing(parentNodeKR, signatureVerificationKR) if err != nil { return nil, 0, nil, err } sessionKey, err := link.GetSessionKey(nodeKR) if err != nil { return nil, 0, nil, err } revision, fileSystemAttrs, err := protonDrive.GetActiveRevisionWithAttrs(ctx, link) if err != nil { return nil, 0, nil, err } reader := &FileDownloadReader{ protonDrive: protonDrive, ctx: ctx, link: link, data: bytes.NewBuffer(nil), nodeKR: nodeKR, sessionKey: sessionKey, revision: revision, nextRevision: 0, isEOF: false, } useFallbackDownload := false if fileSystemAttrs != nil { // based on offset, infer the nextRevision (0-based) if fileSystemAttrs.BlockSizes == nil { useFallbackDownload = true } else { // infer nextRevision totalBytes := int64(0) for i := 0; i < len(fileSystemAttrs.BlockSizes); i++ { prevTotalBytes := totalBytes totalBytes += fileSystemAttrs.BlockSizes[i] if offset <= totalBytes { offset = offset - prevTotalBytes reader.nextRevision = i break } } // download will start from the specified block n, err := io.CopyN(io.Discard, reader, offset) if err != nil { return nil, 0, nil, err } if int64(n) != offset { return nil, 0, nil, ErrSeekOffsetAfterSkippingBlocks } } } if useFallbackDownload { log.Println("Performing inefficient seek as metadata of encrypted file is missing") n, err := io.CopyN(io.Discard, reader, offset) if err != nil { return nil, 0, nil, err } if int64(n) != offset { return nil, 0, nil, ErrSeekOffsetAfterSkippingBlocks } } return reader, link.Size, fileSystemAttrs, nil } Proton-API-Bridge-1.0.0/file_upload.go000066400000000000000000000412541447740121100174130ustar00rootroot00000000000000package proton_api_bridge import ( "bufio" "bytes" "context" "crypto/sha1" "crypto/sha256" "encoding/base64" "encoding/hex" "io" "mime" "os" "path/filepath" "time" "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/henrybear327/go-proton-api" ) func (protonDrive *ProtonDrive) handleRevisionConflict(ctx context.Context, link *proton.Link, createFileResp *proton.CreateFileRes) (string, bool, error) { if link != nil { linkID := link.LinkID draftRevision, err := protonDrive.GetRevisions(ctx, link, proton.RevisionStateDraft) if err != nil { return "", false, err } // if we have a draft revision, depending on the user config, we can abort the upload or recreate a draft // if we have no draft revision, then we can create a new draft revision directly (there is a restriction of 1 draft revision per file) if len(draftRevision) > 0 { // TODO: maintain clientUID to mark that this is our own draft (which can indicate failed upload attempt!) if protonDrive.Config.ReplaceExistingDraft { // Question: how do we observe for file upload cancellation -> clientUID? // Random thoughts: if there are concurrent modification to the draft, the server should be able to catch this when commiting the revision // since the manifestSignature (hash) will fail to match // delete the draft revision (will fail if the file only have a draft but no active revisions) if link.State == proton.LinkStateDraft { // delete the link (skipping trash, otherwise it won't work) err = protonDrive.c.DeleteChildren(ctx, protonDrive.MainShare.ShareID, link.ParentLinkID, linkID) if err != nil { return "", false, err } return "", true, nil } // delete the draft revision err = protonDrive.c.DeleteRevision(ctx, protonDrive.MainShare.ShareID, linkID, draftRevision[0].ID) if err != nil { return "", false, err } } else { // if there is a draft, based on the web behavior, it will ask if the user wants to replace the failed upload attempt // current behavior, we report an error to not upload the file (conservative) return "", false, ErrDraftExists } } // create a new revision newRevision, err := protonDrive.c.CreateRevision(ctx, protonDrive.MainShare.ShareID, linkID) if err != nil { return "", false, err } return newRevision.ID, false, nil } else if createFileResp != nil { return createFileResp.RevisionID, false, nil } else { // should not happen anymore, since the file search will include the draft now return "", false, ErrInternalErrorOnFileUpload } } func (protonDrive *ProtonDrive) createFileUploadDraft(ctx context.Context, parentLink *proton.Link, filename string, modTime time.Time, mimeType string) (string, string, *crypto.SessionKey, *crypto.KeyRing, error) { parentNodeKR, err := protonDrive.getLinkKR(ctx, parentLink) if err != nil { return "", "", nil, nil, err } /* Encryption: parent link's node key Signature: share's signature address keys */ newNodeKey, newNodePassphraseEnc, newNodePassphraseSignature, err := generateNodeKeys(parentNodeKR, protonDrive.DefaultAddrKR) if err != nil { return "", "", nil, nil, err } createFileReq := proton.CreateFileReq{ ParentLinkID: parentLink.LinkID, // Name string // Encrypted File Name // Hash string // Encrypted File Name hash MIMEType: mimeType, // MIME Type // ContentKeyPacket string // The block's key packet, encrypted with the node key. // ContentKeyPacketSignature string // Unencrypted signature of the content session key, signed with the NodeKey NodeKey: newNodeKey, // The private NodeKey, used to decrypt any file/folder content. NodePassphrase: newNodePassphraseEnc, // The passphrase used to unlock the NodeKey, encrypted by the owning Link/Share keyring. NodePassphraseSignature: newNodePassphraseSignature, // The signature of the NodePassphrase SignatureAddress: protonDrive.signatureAddress, // Signature email address used to sign passphrase and name } /* Encryption: parent link's node key Signature: share's signature address keys */ err = createFileReq.SetName(filename, protonDrive.DefaultAddrKR, parentNodeKR) if err != nil { return "", "", nil, nil, err } /* Encryption: parent link's node key Signature: parent link's node key */ signatureVerificationKR, err := protonDrive.getSignatureVerificationKeyring([]string{parentLink.SignatureEmail}, parentNodeKR) if err != nil { return "", "", nil, nil, err } parentHashKey, err := parentLink.GetHashKey(parentNodeKR, signatureVerificationKR) if err != nil { return "", "", nil, nil, err } /* Use parent's hash key */ err = createFileReq.SetHash(filename, parentHashKey) if err != nil { return "", "", nil, nil, err } /* Encryption: parent link's node key Signature: share's signature address keys */ newNodeKR, err := getKeyRing(parentNodeKR, protonDrive.DefaultAddrKR, newNodeKey, newNodePassphraseEnc, newNodePassphraseSignature) if err != nil { return "", "", nil, nil, err } /* Encryption: current link's node key Signature: share's signature address keys */ newSessionKey, err := createFileReq.SetContentKeyPacketAndSignature(newNodeKR) if err != nil { return "", "", nil, nil, err } createFileAction := func() (*proton.CreateFileRes, *proton.Link, error) { createFileResp, err := protonDrive.c.CreateFile(ctx, protonDrive.MainShare.ShareID, createFileReq) if err != nil { // FIXME: check for duplicated filename by relying on checkAvailableHashes -> able to retrieve linkID too // Also saving generating resources such as new nodeKR, etc. if err != proton.ErrFileNameExist { // other real error caught return nil, nil, err } // search for the link within this folder which has an active/draft revision as we have a file creation conflict link, err := protonDrive.SearchByNameInActiveFolder(ctx, parentLink, filename, true, false, proton.LinkStateActive) if err != nil { return nil, nil, err } if link == nil { link, err = protonDrive.SearchByNameInActiveFolder(ctx, parentLink, filename, true, false, proton.LinkStateDraft) if err != nil { return nil, nil, err } if link == nil { // we have a real problem here (unless the assumption is wrong) // since we can't create a new file AND we can't locate a file with active/draft revision in it return nil, nil, ErrCantLocateRevision } } return nil, link, nil } return &createFileResp, nil, nil } createFileResp, link, err := createFileAction() if err != nil { return "", "", nil, nil, err } revisionID, shouldSubmitCreateFileRequestAgain, err := protonDrive.handleRevisionConflict(ctx, link, createFileResp) if err != nil { return "", "", nil, nil, err } if shouldSubmitCreateFileRequestAgain { // the case where the link has only a draft but no active revision // we need to delete the link and recreate one createFileResp, link, err = createFileAction() if err != nil { return "", "", nil, nil, err } revisionID, _, err = protonDrive.handleRevisionConflict(ctx, link, createFileResp) if err != nil { return "", "", nil, nil, err } } linkID := "" if link != nil { linkID = link.LinkID // get original sessionKey and nodeKR for the current link parentNodeKR, err = protonDrive.getLinkKRByID(ctx, link.ParentLinkID) if err != nil { return "", "", nil, nil, err } signatureVerificationKR, err := protonDrive.getSignatureVerificationKeyring([]string{link.SignatureEmail}) if err != nil { return "", "", nil, nil, err } newNodeKR, err = link.GetKeyRing(parentNodeKR, signatureVerificationKR) if err != nil { return "", "", nil, nil, err } newSessionKey, err = link.GetSessionKey(newNodeKR) if err != nil { return "", "", nil, nil, err } } else { linkID = createFileResp.ID } return linkID, revisionID, newSessionKey, newNodeKR, nil } func (protonDrive *ProtonDrive) uploadAndCollectBlockData(ctx context.Context, newSessionKey *crypto.SessionKey, newNodeKR *crypto.KeyRing, file io.Reader, linkID, revisionID string) ([]byte, int64, []int64, string, error) { type PendingUploadBlocks struct { blockUploadInfo proton.BlockUploadInfo encData []byte } if newSessionKey == nil || newNodeKR == nil { return nil, 0, nil, "", ErrMissingInputUploadAndCollectBlockData } totalFileSize := int64(0) pendingUploadBlocks := make([]PendingUploadBlocks, 0) manifestSignatureData := make([]byte, 0) uploadPendingBlocks := func() error { if len(pendingUploadBlocks) == 0 { return nil } blockList := make([]proton.BlockUploadInfo, 0) for i := range pendingUploadBlocks { blockList = append(blockList, pendingUploadBlocks[i].blockUploadInfo) } blockUploadReq := proton.BlockUploadReq{ AddressID: protonDrive.MainShare.AddressID, ShareID: protonDrive.MainShare.ShareID, LinkID: linkID, RevisionID: revisionID, BlockList: blockList, } blockUploadResp, err := protonDrive.c.RequestBlockUpload(ctx, blockUploadReq) if err != nil { return err } errChan := make(chan error) uploadBlockWrapper := func(ctx context.Context, errChan chan error, bareURL, token string, block io.Reader) { // log.Println("Before semaphore") if err := protonDrive.blockUploadSemaphore.Acquire(ctx, 1); err != nil { errChan <- err } defer protonDrive.blockUploadSemaphore.Release(1) // log.Println("After semaphore") // defer log.Println("Release semaphore") errChan <- protonDrive.c.UploadBlock(ctx, bareURL, token, block) } for i := range blockUploadResp { go uploadBlockWrapper(ctx, errChan, blockUploadResp[i].BareURL, blockUploadResp[i].Token, bytes.NewReader(pendingUploadBlocks[i].encData)) } for i := 0; i < len(blockUploadResp); i++ { err := <-errChan if err != nil { return err } } pendingUploadBlocks = pendingUploadBlocks[:0] return nil } shouldContinue := true sha1Digests := sha1.New() blockSizes := make([]int64, 0) for i := 1; shouldContinue; i++ { if (i-1) > 0 && (i-1)%UPLOAD_BATCH_BLOCK_SIZE == 0 { err := uploadPendingBlocks() if err != nil { return nil, 0, nil, "", err } } // read at most data of size UPLOAD_BLOCK_SIZE // for some reason, .Read might not actually read up to buffer size -> use io.ReadFull data := make([]byte, UPLOAD_BLOCK_SIZE) // FIXME: get block size from the server config instead of hardcoding it readBytes, err := io.ReadFull(file, data) if err != nil { if err == io.EOF || err == io.ErrUnexpectedEOF { // might still have data to read! if readBytes == 0 { break } shouldContinue = false } else { // all other errors return nil, 0, nil, "", err } } data = data[:readBytes] totalFileSize += int64(readBytes) sha1Digests.Write(data) blockSizes = append(blockSizes, int64(readBytes)) // encrypt block data /* Encryption: current link's session key Signature: share's signature address keys */ dataPlainMessage := crypto.NewPlainMessage(data) encData, err := newSessionKey.Encrypt(dataPlainMessage) if err != nil { return nil, 0, nil, "", err } encSignature, err := protonDrive.DefaultAddrKR.SignDetachedEncrypted(dataPlainMessage, newNodeKR) if err != nil { return nil, 0, nil, "", err } encSignatureStr, err := encSignature.GetArmored() if err != nil { return nil, 0, nil, "", err } h := sha256.New() h.Write(encData) hash := h.Sum(nil) base64Hash := base64.StdEncoding.EncodeToString(hash) if err != nil { return nil, 0, nil, "", err } manifestSignatureData = append(manifestSignatureData, hash...) pendingUploadBlocks = append(pendingUploadBlocks, PendingUploadBlocks{ blockUploadInfo: proton.BlockUploadInfo{ Index: i, // iOS drive: BE starts with 1 Size: int64(len(encData)), EncSignature: encSignatureStr, Hash: base64Hash, }, encData: encData, }) } err := uploadPendingBlocks() if err != nil { return nil, 0, nil, "", err } sha1Hash := sha1Digests.Sum(nil) sha1String := hex.EncodeToString(sha1Hash) return manifestSignatureData, totalFileSize, blockSizes, sha1String, nil } func (protonDrive *ProtonDrive) commitNewRevision(ctx context.Context, nodeKR *crypto.KeyRing, xAttrCommon *proton.RevisionXAttrCommon, manifestSignatureData []byte, linkID, revisionID string) error { manifestSignature, err := protonDrive.DefaultAddrKR.SignDetached(crypto.NewPlainMessage(manifestSignatureData)) if err != nil { return err } manifestSignatureString, err := manifestSignature.GetArmored() if err != nil { return err } commitRevisionReq := proton.CommitRevisionReq{ ManifestSignature: manifestSignatureString, SignatureAddress: protonDrive.signatureAddress, } err = commitRevisionReq.SetEncXAttrString(protonDrive.DefaultAddrKR, nodeKR, xAttrCommon) if err != nil { return err } err = protonDrive.c.CommitRevision(ctx, protonDrive.MainShare.ShareID, linkID, revisionID, commitRevisionReq) if err != nil { return err } return nil } // testParam is for integration test only // 0 = normal mode // 1 = up to create revision // 2 = up to block upload func (protonDrive *ProtonDrive) uploadFile(ctx context.Context, parentLink *proton.Link, filename string, modTime time.Time, file io.Reader, testParam int) (string, *proton.RevisionXAttrCommon, error) { // TODO: if we should use github.com/gabriel-vasile/mimetype to detect the MIME type from the file content itself // Note: this approach might cause the upload progress to display the "fake" progress, since we read in all the content all-at-once // mimetype.SetLimit(0) // mType := mimetype.Detect(fileContent) // mimeType := mType.String() // detect MIME type by looking at the filename only mimeType := mime.TypeByExtension(filepath.Ext(filename)) if mimeType == "" { // api requires a mime type passed in mimeType = "text/plain" } /* step 1: create a draft */ linkID, revisionID, newSessionKey, newNodeKR, err := protonDrive.createFileUploadDraft(ctx, parentLink, filename, modTime, mimeType) if err != nil { return "", nil, err } if testParam == 1 { return "", nil, nil } /* step 2: upload blocks and collect block data */ manifestSignature, fileSize, blockSizes, digests, err := protonDrive.uploadAndCollectBlockData(ctx, newSessionKey, newNodeKR, file, linkID, revisionID) if err != nil { return "", nil, err } if testParam == 2 { // for integration tests // we try to simulate blocks uploaded but not yet commited return "", nil, nil } /* step 3: mark the file as active by commiting the revision */ xAttrCommon := &proton.RevisionXAttrCommon{ ModificationTime: modTime.Format("2006-01-02T15:04:05-0700"), /* ISO8601 */ Size: fileSize, BlockSizes: blockSizes, Digests: map[string]string{ "SHA1": digests, }, } err = protonDrive.commitNewRevision(ctx, newNodeKR, xAttrCommon, manifestSignature, linkID, revisionID) if err != nil { return "", nil, err } return linkID, xAttrCommon, nil } func (protonDrive *ProtonDrive) UploadFileByReader(ctx context.Context, parentLinkID string, filename string, modTime time.Time, file io.Reader, testParam int) (string, *proton.RevisionXAttrCommon, error) { parentLink, err := protonDrive.getLink(ctx, parentLinkID) if err != nil { return "", nil, err } return protonDrive.uploadFile(ctx, parentLink, filename, modTime, file, testParam) } func (protonDrive *ProtonDrive) UploadFileByPath(ctx context.Context, parentLink *proton.Link, filename string, filePath string, testParam int) (string, *proton.RevisionXAttrCommon, error) { f, err := os.Open(filePath) if err != nil { return "", nil, err } defer f.Close() info, err := os.Stat(filePath) if err != nil { return "", nil, err } in := bufio.NewReader(f) return protonDrive.uploadFile(ctx, parentLink, filename, info.ModTime(), in, testParam) } /* There is a route that proton-go-api doesn't have - checkAvailableHashes. This is used to quickly find the next available filename when the originally supplied filename is taken in the current folder. Based on the code below, which is taken from the Proton iOS Drive app, we can infer that: - when a file is to be uploaded && there is filename conflict after the first upload: - on web, user will be prompted with a) overwrite b) keep both by appending filename with iteration number c) do nothing - on the iOS client logic, we can see that when the filename conflict happens (after the upload attampt failed) - the filename will be hashed by using filename + iteration - 10 iterations will be done per batch, each iteration's hash will be sent to the server - the server will return available hashes, and the client will take the lowest iteration as the filename to be used - will be used to search for the next available filename (using hashes avoids the filename being known to the server) */ Proton-API-Bridge-1.0.0/folder.go000066400000000000000000000171361447740121100164050ustar00rootroot00000000000000package proton_api_bridge import ( "context" "time" "github.com/henrybear327/go-proton-api" ) type ProtonDirectoryData struct { Link *proton.Link Name string IsFolder bool } func (protonDrive *ProtonDrive) ListDirectory( ctx context.Context, folderLinkID string) ([]*ProtonDirectoryData, error) { ret := make([]*ProtonDirectoryData, 0) folderLink, err := protonDrive.getLink(ctx, folderLinkID) if err != nil { return nil, err } if folderLink.State == proton.LinkStateActive { childrenLinks, err := protonDrive.c.ListChildren(ctx, protonDrive.MainShare.ShareID, folderLink.LinkID, true) if err != nil { return nil, err } if childrenLinks != nil { folderParentKR, err := protonDrive.getLinkKRByID(ctx, folderLink.ParentLinkID) if err != nil { return nil, err } signatureVerificationKR, err := protonDrive.getSignatureVerificationKeyring([]string{folderLink.SignatureEmail}) if err != nil { return nil, err } folderLinkKR, err := folderLink.GetKeyRing(folderParentKR, signatureVerificationKR) if err != nil { return nil, err } for i := range childrenLinks { if childrenLinks[i].State != proton.LinkStateActive { continue } signatureVerificationKR, err := protonDrive.getSignatureVerificationKeyring([]string{childrenLinks[i].NameSignatureEmail, childrenLinks[i].SignatureEmail}) if err != nil { return nil, err } name, err := childrenLinks[i].GetName(folderLinkKR, signatureVerificationKR) if err != nil { return nil, err } ret = append(ret, &ProtonDirectoryData{ Link: &childrenLinks[i], Name: name, IsFolder: childrenLinks[i].Type == proton.LinkTypeFolder, }) } } } return ret, nil } func (protonDrive *ProtonDrive) CreateNewFolderByID(ctx context.Context, parentLinkID string, folderName string) (string, error) { /* It's like event system, we need to get the latest information before creating the move request! */ protonDrive.removeLinkIDFromCache(parentLinkID, false) parentLink, err := protonDrive.getLink(ctx, parentLinkID) if err != nil { return "", err } return protonDrive.CreateNewFolder(ctx, parentLink, folderName) } func (protonDrive *ProtonDrive) CreateNewFolder(ctx context.Context, parentLink *proton.Link, folderName string) (string, error) { parentNodeKR, err := protonDrive.getLinkKR(ctx, parentLink) if err != nil { return "", err } newNodeKey, newNodePassphraseEnc, newNodePassphraseSignature, err := generateNodeKeys(parentNodeKR, protonDrive.DefaultAddrKR) if err != nil { return "", err } createFolderReq := proton.CreateFolderReq{ ParentLinkID: parentLink.LinkID, // Name string // Hash string NodeKey: newNodeKey, // NodeHashKey string NodePassphrase: newNodePassphraseEnc, NodePassphraseSignature: newNodePassphraseSignature, SignatureAddress: protonDrive.signatureAddress, } /* Name is encrypted using the parent's keyring, and signed with address key */ err = createFolderReq.SetName(folderName, protonDrive.DefaultAddrKR, parentNodeKR) if err != nil { return "", err } signatureVerificationKR, err := protonDrive.getSignatureVerificationKeyring([]string{parentLink.SignatureEmail}, parentNodeKR) if err != nil { return "", err } parentHashKey, err := parentLink.GetHashKey(parentNodeKR, signatureVerificationKR) if err != nil { return "", err } err = createFolderReq.SetHash(folderName, parentHashKey) if err != nil { return "", err } newNodeKR, err := getKeyRing(parentNodeKR, protonDrive.DefaultAddrKR, newNodeKey, newNodePassphraseEnc, newNodePassphraseSignature) if err != nil { return "", err } err = createFolderReq.SetNodeHashKey(newNodeKR) if err != nil { return "", err } // FIXME: check for duplicated filename by relying on checkAvailableHashes // if the folder name already exist, this call will return an error createFolderResp, err := protonDrive.c.CreateFolder(ctx, protonDrive.MainShare.ShareID, createFolderReq) if err != nil { return "", err } // log.Printf("createFolderResp %#v", createFolderResp) return createFolderResp.ID, nil } func (protonDrive *ProtonDrive) MoveFileByID(ctx context.Context, srcLinkID, dstParentLinkID string, dstName string) error { /* It's like event system, we need to get the latest information before creating the move request! */ protonDrive.removeLinkIDFromCache(srcLinkID, false) srcLink, err := protonDrive.getLink(ctx, srcLinkID) if err != nil { return err } if srcLink.State != proton.LinkStateActive { return ErrLinkMustBeActive } dstParentLink, err := protonDrive.getLink(ctx, dstParentLinkID) if err != nil { return err } if dstParentLink.State != proton.LinkStateActive { return ErrLinkMustBeActive } return protonDrive.MoveFile(ctx, srcLink, dstParentLink, dstName) } func (protonDrive *ProtonDrive) MoveFile(ctx context.Context, srcLink *proton.Link, dstParentLink *proton.Link, dstName string) error { return protonDrive.moveLink(ctx, srcLink, dstParentLink, dstName) } func (protonDrive *ProtonDrive) MoveFolderByID(ctx context.Context, srcLinkID, dstParentLinkID, dstName string) error { /* It's like event system, we need to get the latest information before creating the move request! */ protonDrive.removeLinkIDFromCache(srcLinkID, false) srcLink, err := protonDrive.getLink(ctx, srcLinkID) if err != nil { return err } if srcLink.State != proton.LinkStateActive { return ErrLinkMustBeActive } dstParentLink, err := protonDrive.getLink(ctx, dstParentLinkID) if err != nil { return err } if dstParentLink.State != proton.LinkStateActive { return ErrLinkMustBeActive } return protonDrive.MoveFolder(ctx, srcLink, dstParentLink, dstName) } func (protonDrive *ProtonDrive) MoveFolder(ctx context.Context, srcLink *proton.Link, dstParentLink *proton.Link, dstName string) error { return protonDrive.moveLink(ctx, srcLink, dstParentLink, dstName) } func (protonDrive *ProtonDrive) moveLink(ctx context.Context, srcLink *proton.Link, dstParentLink *proton.Link, dstName string) error { // we are moving the srcLink to under dstParentLink, with name dstName req := proton.MoveLinkReq{ ParentLinkID: dstParentLink.LinkID, OriginalHash: srcLink.Hash, SignatureAddress: protonDrive.signatureAddress, } dstParentKR, err := protonDrive.getLinkKR(ctx, dstParentLink) if err != nil { return err } err = req.SetName(dstName, protonDrive.DefaultAddrKR, dstParentKR) if err != nil { return err } signatureVerificationKR, err := protonDrive.getSignatureVerificationKeyring([]string{dstParentLink.SignatureEmail}, dstParentKR) if err != nil { return err } dstParentHashKey, err := dstParentLink.GetHashKey(dstParentKR, signatureVerificationKR) if err != nil { return err } err = req.SetHash(dstName, dstParentHashKey) if err != nil { return err } srcParentKR, err := protonDrive.getLinkKRByID(ctx, srcLink.ParentLinkID) if err != nil { return err } nodePassphrase, err := reencryptKeyPacket(srcParentKR, dstParentKR, protonDrive.DefaultAddrKR, srcLink.NodePassphrase) if err != nil { return err } req.NodePassphrase = nodePassphrase req.NodePassphraseSignature = srcLink.NodePassphraseSignature protonDrive.removeLinkIDFromCache(srcLink.LinkID, false) // TODO: disable cache when move is in action? // because there might be the case where others read for the same link currently being move -> race condition // argument: cache itself is already outdated in a sense, as we don't even have event system (even if we have, it's still outdated...) err = protonDrive.c.MoveLink(ctx, protonDrive.MainShare.ShareID, srcLink.LinkID, req) if err != nil { return err } time.Sleep(5 * time.Second) return nil } Proton-API-Bridge-1.0.0/folder_recursive.go000066400000000000000000000054701447740121100204720ustar00rootroot00000000000000package proton_api_bridge import ( "context" "io" "log" "os" "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/henrybear327/go-proton-api" ) func (protonDrive *ProtonDrive) listDirectoriesRecursively( ctx context.Context, parentNodeKR *crypto.KeyRing, link *proton.Link, download bool, maxDepth, curDepth /* 0-based */ int, excludeRoot bool, pathSoFar string, paths *[]string) error { /* Assumptions: - we only care about the active ones */ if link.State != proton.LinkStateActive { return nil } // log.Println("curDepth", curDepth, "pathSoFar", pathSoFar) var currentPath = "" if !(excludeRoot && curDepth == 0) { signatureVerificationKR, err := protonDrive.getSignatureVerificationKeyring([]string{link.NameSignatureEmail, link.SignatureEmail}) if err != nil { return err } name, err := link.GetName(parentNodeKR, signatureVerificationKR) if err != nil { return err } currentPath = pathSoFar + "/" + name // log.Println("currentPath", currentPath) if paths != nil { *paths = append(*paths, currentPath) } } if download { if protonDrive.Config.DataFolderName == "" { return ErrDataFolderNameIsEmpty } if link.Type == proton.LinkTypeFile { log.Println("Downloading", currentPath) defer log.Println("Completes downloading", currentPath) reader, _, _, err := protonDrive.DownloadFile(ctx, link, 0) if err != nil { return err } byteArray, err := io.ReadAll(reader) if err != nil { return err } err = os.WriteFile("./"+protonDrive.Config.DataFolderName+"/"+currentPath, byteArray, 0777) if err != nil { return err } } else /* folder */ { if !(excludeRoot && curDepth == 0) { // log.Println("Creating folder", currentPath) // defer log.Println("Completes creating folder", currentPath) err := os.Mkdir("./"+protonDrive.Config.DataFolderName+"/"+currentPath, 0777) if err != nil { return err } } } } if maxDepth == -1 || curDepth < maxDepth { if link.Type == proton.LinkTypeFolder { childrenLinks, err := protonDrive.c.ListChildren(ctx, protonDrive.MainShare.ShareID, link.LinkID, true) if err != nil { return err } // log.Printf("childrenLinks len = %v, %#v", len(childrenLinks), childrenLinks) if childrenLinks != nil { // get current node's keyring signatureVerificationKR, err := protonDrive.getSignatureVerificationKeyring([]string{link.SignatureEmail}) if err != nil { return err } linkKR, err := link.GetKeyRing(parentNodeKR, signatureVerificationKR) if err != nil { return err } for _, childLink := range childrenLinks { err = protonDrive.listDirectoriesRecursively(ctx, linkKR, &childLink, download, maxDepth, curDepth+1, excludeRoot, currentPath, paths) if err != nil { return err } } } } } return nil } Proton-API-Bridge-1.0.0/go.mod000066400000000000000000000027101447740121100157010ustar00rootroot00000000000000module github.com/henrybear327/Proton-API-Bridge go 1.18 require ( github.com/ProtonMail/gluon v0.17.1-0.20230724134000-308be39be96e github.com/ProtonMail/gopenpgp/v2 v2.7.3 github.com/henrybear327/go-proton-api v0.0.0-20230907193451-e563407504ce github.com/relvacode/iso8601 v1.3.0 golang.org/x/sync v0.3.0 ) require ( github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect github.com/ProtonMail/go-srp v0.0.7 // indirect github.com/PuerkitoBio/goquery v1.8.1 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect github.com/bradenaw/juniper v0.13.1 // indirect github.com/cloudflare/circl v1.3.3 // indirect github.com/cronokirby/saferith v0.33.0 // indirect github.com/emersion/go-message v0.17.0 // indirect github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 // indirect github.com/go-resty/resty/v2 v2.7.0 // indirect github.com/google/uuid v1.3.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect golang.org/x/crypto v0.13.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/net v0.15.0 // indirect golang.org/x/sys v0.12.0 // indirect golang.org/x/text v0.13.0 // indirect ) Proton-API-Bridge-1.0.0/go.sum000066400000000000000000000322551447740121100157350ustar00rootroot00000000000000github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I= github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug= github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo= github.com/ProtonMail/gluon v0.17.1-0.20230724134000-308be39be96e h1:lCsqUUACrcMC83lg5rTo9Y0PnPItE61JSfvMyIcANwk= github.com/ProtonMail/gluon v0.17.1-0.20230724134000-308be39be96e/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo= github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE= github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI= github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk= github.com/ProtonMail/gopenpgp/v2 v2.7.3 h1:AJu1OI/1UWVYZl6QcCLKGu9OTngS2r52618uGlje84I= github.com/ProtonMail/gopenpgp/v2 v2.7.3/go.mod h1:IhkNEDaxec6NyzSI0PlxapinnwPVIESk8/76da3Ct3g= github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/bradenaw/juniper v0.13.1 h1:9P7/xeaYuEyqPuJHSHCJoisWyPvZH4FAi59BxJLh7F8= github.com/bradenaw/juniper v0.13.1/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo= github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emersion/go-message v0.17.0 h1:NIdSKHiVUx4qKqdd0HyJFD41cW8iFguM2XJnRZWQH04= github.com/emersion/go-message v0.17.0/go.mod h1:/9Bazlb1jwUNB0npYYBsdJ2EMOiiyN3m5UVHbY7GoNw= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 h1:ATgqloALX6cHCranzkLb8/zjivwQ9DWWDCQRnxTPfaA= github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/henrybear327/go-proton-api v0.0.0-20230907193451-e563407504ce h1:n1URi7VYiwX/3akX51keQXi6Huy4lJdVc4biJHYk3iw= github.com/henrybear327/go-proton-api v0.0.0-20230907193451-e563407504ce/go.mod h1:w63MZuzufKcIZ93pwRgiOtxMXYafI8H74D77AxytOBc= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/relvacode/iso8601 v1.3.0 h1:HguUjsGpIMh/zsTczGN3DVJFxTU/GX+MMmzcKoMO7ko= github.com/relvacode/iso8601 v1.3.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= Proton-API-Bridge-1.0.0/mail.go000066400000000000000000000117751447740121100160570ustar00rootroot00000000000000package proton_api_bridge import ( "context" "encoding/base64" "io/ioutil" "log" "net/mail" "path/filepath" "github.com/ProtonMail/gluon/rfc822" "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/henrybear327/go-proton-api" ) type MailSendingParameters struct { TemplateFile string EmailSubject string RecipientEmailAddress string EmailAttachments []string EmailContentIDs []string } func (protonDrive *ProtonDrive) SendEmail(ctx context.Context, i int, errChan chan error, config *MailSendingParameters) { log.Println("SendEmail in", i) defer log.Println("SendEmail out", i) createDraftResp, err := protonDrive.createDraft(ctx, config) if err != nil { errChan <- err } attachments, err := protonDrive.uploadAttachments(ctx, createDraftResp, config) if err != nil { errChan <- err } err = protonDrive.sendDraft(ctx, createDraftResp.ID, attachments, config) if err != nil { errChan <- err } errChan <- nil } func (protonDrive *ProtonDrive) getHTMLBody(config *MailSendingParameters) ([]byte, error) { htmlTemplate, err := ioutil.ReadFile(config.TemplateFile) if err != nil { return nil, err } return htmlTemplate, nil } func (protonDrive *ProtonDrive) createDraft(ctx context.Context, config *MailSendingParameters) (*proton.Message, error) { htmlTemplate, err := protonDrive.getHTMLBody(config) if err != nil { return nil, err } createDraftReq := proton.CreateDraftReq{ Message: proton.DraftTemplate{ Subject: config.EmailSubject, Sender: &mail.Address{ Address: protonDrive.signatureAddress, }, ToList: []*mail.Address{ { Address: config.RecipientEmailAddress, }, }, CCList: []*mail.Address{}, BCCList: []*mail.Address{}, Body: string(htmlTemplate), // NOTE: the body here is for yourself to view it! No sender encryption is done yet MIMEType: rfc822.TextHTML, // Unread: false, // ExternalID: "", // FIXME: what's this }, } createDraftResp, err := protonDrive.c.CreateDraft(ctx, protonDrive.DefaultAddrKR, createDraftReq) if err != nil { return nil, err } return &createDraftResp, nil } func (protonDrive *ProtonDrive) getAttachmentSessionKeyMap(attachments []*proton.Attachment) (map[string]*crypto.SessionKey, error) { ret := make(map[string]*crypto.SessionKey) for i := range attachments { keyPacket, err := base64.StdEncoding.DecodeString(attachments[i].KeyPackets) if err != nil { return nil, err } key, err := protonDrive.DefaultAddrKR.DecryptSessionKey(keyPacket) if err != nil { return nil, err } ret[attachments[i].ID] = key } return ret, nil } func (protonDrive *ProtonDrive) uploadAttachments(ctx context.Context, createDraftResp *proton.Message, config *MailSendingParameters) ([]*proton.Attachment, error) { attachments := make([]*proton.Attachment, 0) for i := range config.EmailAttachments { // read out attachment file fileByteArray, err := ioutil.ReadFile(config.EmailAttachments[i]) if err != nil { return nil, err } req := proton.CreateAttachmentReq{ MessageID: createDraftResp.ID, Filename: filepath.Base(config.EmailAttachments[i]), MIMEType: rfc822.MultipartMixed, // FIXME: what is this? Disposition: proton.InlineDisposition, ContentID: config.EmailContentIDs[i], Body: fileByteArray, } uploadAttachmentResp, err := protonDrive.c.UploadAttachment(ctx, protonDrive.DefaultAddrKR, req) if err != nil { return nil, err } // log.Printf("uploadAttachmentResp %#v", uploadAttachmentResp) attachments = append(attachments, &uploadAttachmentResp) } return attachments, nil } func (protonDrive *ProtonDrive) sendDraft(ctx context.Context, messageID string, attachents []*proton.Attachment, config *MailSendingParameters) error { // FIXME: repect all sendPrefs // FIXME: respect PGPMIMEScheme, etc. recipientPublicKeys, recipientType, err := protonDrive.c.GetPublicKeys(ctx, config.RecipientEmailAddress) if err != nil { return err } if recipientType != proton.RecipientTypeInternal { log.Fatalln("Currently only support internal email sending") } recipientKR, err := recipientPublicKeys.GetKeyRing() if err != nil { return err } htmlTemplate, err := protonDrive.getHTMLBody(config) if err != nil { return err } atts, err := protonDrive.getAttachmentSessionKeyMap(attachents) if err != nil { return err } // send email sendReq := proton.SendDraftReq{ // Packages: []*proton.MessagePackage{}, } // for each of the recipient, we encrypt body for them if err = sendReq.AddTextPackage(protonDrive.DefaultAddrKR, string(htmlTemplate), rfc822.TextHTML, map[string]proton.SendPreferences{config.RecipientEmailAddress: { Encrypt: true, PubKey: recipientKR, SignatureType: proton.DetachedSignature, EncryptionScheme: proton.InternalScheme, MIMEType: rfc822.TextHTML, // FIXME }}, atts, ); err != nil { return err } /* msg */ _, err = protonDrive.c.SendDraft(ctx, messageID, sendReq) if err != nil { return err } // log.Println(msg) return nil } Proton-API-Bridge-1.0.0/search.go000066400000000000000000000061031447740121100163670ustar00rootroot00000000000000package proton_api_bridge import ( "context" "github.com/henrybear327/go-proton-api" ) /* The filename is unique in a given folder, since it's checked (by using hash) on the server */ // if the target isn't found, nil will be returned for both return values func (protonDrive *ProtonDrive) SearchByNameInActiveFolderByID(ctx context.Context, folderLinkID string, targetName string, searchForFile, searchForFolder bool, targetState proton.LinkState) (*proton.Link, error) { folderLink, err := protonDrive.getLink(ctx, folderLinkID) if err != nil { return nil, err } return protonDrive.SearchByNameInActiveFolder(ctx, folderLink, targetName, searchForFile, searchForFolder, targetState) } func (protonDrive *ProtonDrive) SearchByNameInActiveFolder( ctx context.Context, folderLink *proton.Link, targetName string, searchForFile, searchForFolder bool, targetState proton.LinkState) (*proton.Link, error) { if !searchForFile && !searchForFolder { // nothing to search return nil, nil } // we search all folders and files within this designated folder only if folderLink.Type != proton.LinkTypeFolder { return nil, ErrLinkTypeMustToBeFolderType } if folderLink.State != proton.LinkStateActive { // we only search in the active folders return nil, nil } // get target name Hash parentNodeKR, err := protonDrive.getLinkKRByID(ctx, folderLink.ParentLinkID) if err != nil { return nil, err } signatureVerificationKR, err := protonDrive.getSignatureVerificationKeyring([]string{folderLink.SignatureEmail}) if err != nil { return nil, err } folderLinkKR, err := folderLink.GetKeyRing(parentNodeKR, signatureVerificationKR) if err != nil { return nil, err } signatureVerificationKR, err = protonDrive.getSignatureVerificationKeyring([]string{folderLink.SignatureEmail}, folderLinkKR) if err != nil { return nil, err } folderHashKey, err := folderLink.GetHashKey(folderLinkKR, signatureVerificationKR) if err != nil { return nil, err } targetNameHash, err := proton.GetNameHash(targetName, folderHashKey) if err != nil { return nil, err } // use available hash to check if it exists // more efficient than linear scan to just do existence check // used in rclone when Put(), it will try to see if the object exists or not res, err := protonDrive.c.CheckAvailableHashes(ctx, protonDrive.MainShare.ShareID, folderLink.LinkID, proton.CheckAvailableHashesReq{ Hashes: []string{targetNameHash}, }) if err != nil { return nil, err } if len(res.AvailableHashes) == 1 { // name isn't taken == name doesn't exist return nil, nil } childrenLinks, err := protonDrive.c.ListChildren(ctx, protonDrive.MainShare.ShareID, folderLink.LinkID, true) if err != nil { return nil, err } for _, childLink := range childrenLinks { if childLink.State != targetState { continue } if searchForFile && childLink.Type == proton.LinkTypeFile && childLink.Hash == targetNameHash { return &childLink, nil } else if searchForFolder && childLink.Type == proton.LinkTypeFolder && childLink.Hash == targetNameHash { return &childLink, nil } } return nil, nil } Proton-API-Bridge-1.0.0/search_recursive.go000066400000000000000000000072331447740121100204630ustar00rootroot00000000000000package proton_api_bridge import ( "context" "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/henrybear327/go-proton-api" ) func (protonDrive *ProtonDrive) searchByNameRecursivelyFromRoot(ctx context.Context, targetName string, isFolder bool, listAllActiveOrDraftFiles bool) (*proton.Link, error) { var linkType proton.LinkType if isFolder { linkType = proton.LinkTypeFolder } else { linkType = proton.LinkTypeFile } return protonDrive.performSearchByNameRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, targetName, linkType, listAllActiveOrDraftFiles) } // func (protonDrive *ProtonDrive) searchByNameRecursivelyByID(ctx context.Context, folderLinkID string, targetName string, isFolder bool, listAllActiveOrDraftFiles bool) (*proton.Link, error) { // folderLink, err := protonDrive.getLink(ctx, folderLinkID) // if err != nil { // return nil, err // } // var linkType proton.LinkType // if isFolder { // linkType = proton.LinkTypeFolder // } else { // linkType = proton.LinkTypeFile // } // if folderLink.Type != proton.LinkTypeFolder { // return nil, ErrLinkTypeMustToBeFolderType // } // folderKeyRing, err := protonDrive.getLinkKRByID(ctx, folderLink.ParentLinkID) // if err != nil { // return nil, err // } // return protonDrive.performSearchByNameRecursively(ctx, folderKeyRing, folderLink, targetName, linkType, listAllActiveOrDraftFiles) // } func (protonDrive *ProtonDrive) SearchByNameRecursively(ctx context.Context, folderLink *proton.Link, targetName string, isFolder bool, listAllActiveOrDraftFiles bool) (*proton.Link, error) { var linkType proton.LinkType if isFolder { linkType = proton.LinkTypeFolder } else { linkType = proton.LinkTypeFile } if folderLink.Type != proton.LinkTypeFolder { return nil, ErrLinkTypeMustToBeFolderType } folderKeyRing, err := protonDrive.getLinkKRByID(ctx, folderLink.ParentLinkID) if err != nil { return nil, err } return protonDrive.performSearchByNameRecursively(ctx, folderKeyRing, folderLink, targetName, linkType, listAllActiveOrDraftFiles) } func (protonDrive *ProtonDrive) performSearchByNameRecursively( ctx context.Context, parentNodeKR *crypto.KeyRing, link *proton.Link, targetName string, linkType proton.LinkType, listAllActiveOrDraftFiles bool) (*proton.Link, error) { if listAllActiveOrDraftFiles { if link.State != proton.LinkStateActive && link.State != proton.LinkStateDraft { return nil, nil } } else if link.State != proton.LinkStateActive { return nil, nil } signatureVerificationKR, err := protonDrive.getSignatureVerificationKeyring([]string{link.NameSignatureEmail, link.SignatureEmail}) if err != nil { return nil, err } name, err := link.GetName(parentNodeKR, signatureVerificationKR) if err != nil { return nil, err } if link.Type == linkType && name == targetName { return link, nil } if link.Type == proton.LinkTypeFolder { childrenLinks, err := protonDrive.c.ListChildren(ctx, protonDrive.MainShare.ShareID, link.LinkID, true) if err != nil { return nil, err } // log.Printf("childrenLinks len = %v, %#v", len(childrenLinks), childrenLinks) // get current node's keyring signatureVerificationKR, err := protonDrive.getSignatureVerificationKeyring([]string{link.SignatureEmail}) if err != nil { return nil, err } linkKR, err := link.GetKeyRing(parentNodeKR, signatureVerificationKR) if err != nil { return nil, err } for _, childLink := range childrenLinks { ret, err := protonDrive.performSearchByNameRecursively(ctx, linkKR, &childLink, targetName, linkType, listAllActiveOrDraftFiles) if err != nil { return nil, err } if ret != nil { return ret, nil } } } return nil, nil } Proton-API-Bridge-1.0.0/shares.go000066400000000000000000000007441447740121100164140ustar00rootroot00000000000000package proton_api_bridge import ( "context" "github.com/henrybear327/go-proton-api" ) func getAllShares(ctx context.Context, c *proton.Client) ([]proton.ShareMetadata, error) { shares, err := c.ListShares(ctx, true) if err != nil { return nil, err } return shares, nil } func getShareByID(ctx context.Context, c *proton.Client, shareID string) (*proton.Share, error) { share, err := c.GetShare(ctx, shareID) if err != nil { return nil, err } return &share, nil } Proton-API-Bridge-1.0.0/testcase/000077500000000000000000000000001447740121100164065ustar00rootroot00000000000000Proton-API-Bridge-1.0.0/testcase/empty.txt000066400000000000000000000000001447740121100202730ustar00rootroot00000000000000Proton-API-Bridge-1.0.0/testcase/integrationTestImage.png000066400000000000000000001407151447740121100232520ustar00rootroot00000000000000PNG  IHDRp=g iCCPICC ProfileHP)7A:(HQ IM,ZP(@EnEDY ~`y;3gΗs9ν3SaeDYP?/ztL,7H@qbfHH@d]>Єc9grBNer>KXj7Op+T =, Nd4 e!LOp$HtO <$OE<a1iiD~Fwy˙ a,IJ̑ԕDI|<~Rx4g #LsfJXL KHCE~^3u}{OnB|mVR|"Lhyo(Sam 3`&ËaÅF%& W P4@PXT"JZ*FP&TJB}Bc4mvE#\tzz}]nEA0dac1LS99c>`X`˰u؋Nlvi,pn`+]uq x]-G]A`Dp!x<&AB60FT!݈djb9xNAAA_YaPaB k H$sG6.ޑdc'9EH&_&?%T(Z)y++_+JKʔN)VR&(+9++*WQبlP9r]*NXGZz@jE1(\ZAJ?K5qj;uXMU^-R-WR휚شT&I=YڳϪ5kT}:_XN[]G#EcFMB=W4fSg.}rC-X\+Tk[Z#:~bڗth:::t Rtut/辤љTz9>'ۯ׮7oFNрah͠`PpaaC#(hQѨq:&&l|ǦdS *ӻfX3Yns<ɼlh!m93yhN՜$Keee*jU빆scn6uAG666kllښrm+mڑ|V5ڽq8wXQX8d>al`\s8{9tnvr/WKף/;8MߍMNww.xTy<4y`1ǘ$^gFY.嬋(o?bvU a?e~1[ﳵ\v5{8)`y@k )0,"Yy$i>_HތN[FBF~H06ZQOOm>|})j[ib39 x{r d~X45OO 4 0I?=)"fb,b]ƫ܈ND;9Ob9AU25?-ji<\eV8eXIfMM*>F(iNxpASCIIScreenshotli-N pHYs%%IR$iTXtXML:com.adobe.xmp 230 880 Screenshot wiDOTs(sse%fv@IDATx]xU 5K** ("*(߂RPDETPP, *ҋ";II{mf'MHNylvp7::DA@A@A@R<BR5    '7   !pB4A@A@A@'   !pB4A@A@A@'   !pB4A@A@A@;y떠* -2LRg2SA@TT{d₀ 83gFDA@!pI)@*@@/8)  8G@s\ =B{OMA9B"gA@HNe˨0k*wEi=xФqce>WjUZagyXnşG;蓿lK2p&֫EG (**XTzg>78`}3mϟ:$#A@_HVe&:)EZ؆gp($7˙93umP:2*N\Lhu.rezIE%~?NՙЈQh?{"?KU6k9%&pӦnukNPA4aC~[0] )m,7{'hYAk֮MʩK߂ d#p1wchAytTT&jJG \I~#>woXN17t#"BPP~=1|8}4aeDlrܹs*UXtޝ> OyTnؐtUYpL=Z|T7{i>֭[X3.A@R#IJ8KWn+\"Dоy{~)wv2rw|<ᘪ|>ʒ?>!k*T0]:p.@WO] vn/֠8*>n×ʱ+t (R( rҹ9N~#_#8*ZⰃ U)^_YnqDlJsy:S@e29CB&΄Q`Az8v΄S1hVoܽngi[B,ôΐ.年^ҧMosuu Dw,Μʕcq 3f7=hࠉ9o֧a{UZs'e?? >0yf%J{%m`}v+٬6kqL4v8| .Lp4z,9;(>bklw3$rƢD{Y#͛B;/qFnތ6snPJ&HKjc_z(NfvQ4._!ի_~;5mfОmXRի\9/ɛNF{\Cw,|<԰QU)O%[ T5pdKjMht矋6.Jxcy:tU_3|(kZ?હB,&A@HRt;F!ܴQKUs#M~,Vlؿf*zVG 56%b}(rXKW$>zf  ڽpLT3~x[!{Ӧ43L{ONu;@_c&.-̤(RhXڷ2 vxկ|La<~j3mlb?Q<$l…hV{w;pEXY_bgjg5'9z TCe }|9e;gҭQ#Ɂ1b]׏>R~o Fɟ=;fL$}~~ Z_W;/hG4tGlr9UݿGٲe=jX%3[6gEE6QX~zGiscw;#G7bų;T=r:#K&+]#q^|7 JڏWULkVa1A^W7 Or }]]'L{̡O_nߞ֮Y#8_ g$=Dd̶tMJNK8XZA[@xayZ?U*^SFGm~Ԟ2fŃG QւYUʂjrPa-GD̍ӥ\(OuPΜ-3j:{ܙ_׌uBXLgtv*;7U] A/i/k Xۧgl( c#p8 :?ط> e^  5Ay5L_hxO[m ƆK\(^m`}_0U\/z}+8{8#GҀuG۟~QCH)N/1͛N\T5RoEQhhҊk_< h#!Иzd͝ fG]VP& Ԯ|i!mҶ]-~nqC ~^NLA >wQEDELM- o3AWSNU#σ3ʠsS>GHm.hݺ5" H:gw{+)H\ ll*?<"~ &?Jiҧ/=7878-^C@6JCtU#_t&ƅy^KiMҢg}TUYJ9O5&O} gG*y5HܟȆhg4{<|:U=,APZ>]5?T_psQEe<rC;.l"'z@`!Qf (>U͜ak*?e}K_}H56}B 2{565 4dWoR&V -HӸ\dڶlZsk>37+r5$w蠋}Ko-@5"徵mX&%@j*eJ1~IaEŗk_t Jp<-ZWwKү5z}J Bࠅw.kW*u-_nS[N,PzFa5OOn>AEկС"UxM쎟?Ck?) o;o^xb906>`iU]0f"0ɠY!pOLJ_#3_F]39c&[%KI!=A_4?xFJ7_8_ 2Dnx%w_sW`Uʫ{着W5õ}>-ZTT9|-9U.wfjǺVjM)mc58m}|a,RZYs[Ch߆؝4sF4K Ao#L2?Ђ| bZ/8@M$kBY>ZtJ%tqrRA?@kgџCm>  9\KT~1 3SDh*߾UOvI$aNQ)F: FI!CmoѷNmrC8wl>}yG7^2_K&whزq -x ɶG7}(SDk+Mo ؇MtSG2[?# 1z*ȱvMAfI>Mܭe>|iY]H E=vv3=w{p,gƼ\\|_'O#Ys&%_o ߧqܟ?D8#He냮*Q#9s:1b{su ⋽'0A2q{2E1cZvJ\o ߇I,߇IR cjϿP4M֩[Z'/J46tA@4BNuj" eKޭ'Eګ<^T]4P後0~n<>]s4{7Η @eä 0|t5cu}uØjAODl~1wj;M̦zl%Lmr/Vp~kokV|u=W=0;РvUիF&?eoʊDN. #!8IMv?v ]=@j]*& ?Q->-2?~}oNX|>Pzʿ.C4d[j2~g k۱W 4 ;oRZ5Z|xU| gHs ]v?7_4$ i\^*'62f#f2AQl_u9ܥC;7t8iluu mzI)QCreB?; b`4i48K^I#1 WL8_}M\1n}]L_6pQ!aV AXi #ZY$ȉjb>3r5 =h<~4"C&p^/Msu?9@R{ d.NLbqoWmiթ/:}hӮ4=E߇/و=2.?vl?|F 16QAs:7&8u>l:&35דcA@B~FdCx&7uU'l6U𧏨~g&ntNz꿨~ZuQ'~ZR}*RC; X,<8>`%#F|~5?b"V}OOG?"\/1AT`>p?o/B>#CQf7&pτ_Yr27&HrZό}'=ŗE(7Wj+{M~.hk|?TC)18cy)ZD]TE-G:7VkY+m3v*˜-{Ne3h z@'U\pMRZ`$F9Ʊ&2dX*]f 6Y;y&jwӒŇ[SV?O2ѿ/ڱď?`rBA࿉@8~;cwosr`Xk?rt; Ȣc"@Os6Io^+Z\‘-٥1%L.az r}RI-\L/d~pFAn8Bnm1G$L!:ʃ/x@@Lagf_=!p8\zNz8Oݸ}C#9:d: E'⮝&pH-eM @Xt[l~1]ƶZ%FsgAk_UUSgD~5fd U#Ӈ/B>! BYUhpN84Ar=f]Ј6D#\Yy'z./ol_b<ϭL"9oȦ{;,$g3vVs8_zW/R.hɀWҕ+>ds1G&~CY{~OP|^!sf:iPMG]qDkQ٘ -zu}9kEA@ pXOK1vnw0D7#d"Cho)K}`@7'tTADEҺˤ)'ZtspZ@8LqocP8bah4}B|m!<#GL R>ʕ\u(wEP,D@>=}sFp&VB|Y!I9EwxVrO?p*_vF6`wоm 2GIXt_i~v=hkq"n##/ytj,if8bK# "5;z !Q7 Qmљh! `3b,gϜN Z^Wjqޯď6mToWAԝ_k*S6 D7odj&? ɻ+׮9NŘ \m_t @ `ԩr*%3kVШ!ewG\Pf ̧Tyi--j/g-:G&ڴ?h![m0W"P vշ?9yR28h?ޜ9B4t03D " ҴbEҹLJk遽y F&PB6hS X<-lܢ[ 1Q9ʭ e}cΥl@Μtu5Wj;#Cy5!7?~EOou:> 8 r&Or^80v] 0X}Xd4E/'bvFU6w]qwr_Tet4x>?~?J/NuBYXSXGm GEAY#}>9Ae>aLxNd978H5 2FzG3itH8HlT9rf$a-Qnq saA@A }@|m^SLj+xaŻj,9='MJ}䅃@6ptK/QyվD7F@\,GV e2d 5 mh_qDʸ.{N9͛_ +Tul~z|_b 7flNdС4qD*߅L2TY~n ܑ-ˤnx.sZrn,its^9͛7Cq9NΝFr҆u%(A@HryюӔsey (G`uvH0<|(U9FE#!+"%\jA(o_Ptmt"p  @@\2>8I0Mmzr"@ gq1!pί}90K _&kX؃8#/ TK*d=fd 2m:ͤ ~A@{yA]"8U.&oHNCGA@ &&   8" 9A@A@A " .^   sCA@A@A@HKF&& qO'MR+i:B3]160Ŭ}W:4Mr^ga˕Pٲe3]֓A@RQwxhsL:!p}ee5R{xaD4s`Lٺ:k|=M̙3ԹS'W^}f\AR@L P칭 ^=P&pÔ-HC Y(KAȫ{x*G(Pfo*Uf" J$6G7QIh9tr=|ĄɅS@2 \w+K1;݀|LXpΰwT=8<% kҠl ]qVͧsMp#ڞ|"9IX֊#t l|i8k@nTCwciK!E\*)C:58o'F7gfs4'IZXf>YSwf͚Ŕ)8eIbOo>UmkJ{O/S@. ŋ)~ɗ/}ǺȾC>02v>ڿF_7 _իWcǪ/P2o4hZREb]xzo/0-߉M [pjC\\"U T@I&}Z]61w-3~\Ll:M+w' ; Vϳ\i g.0j\>"} %]@ծ*+İi笍h' 򝶹 Id4S0LR͊;sr D>L@x΄GS,⯽w%pc#p8@^7VPpVzeI#nҷ)YeC` ei ̖Kyz U=bvNWEzU,kbmkW&~ \ &'!fϦ`/ 6^+!齻lGk'GbG &0t}q*P}<8[lԪuk0h|m_}+w}!H1&K'KZiC탦 }FFF* /sP\A :<Ĭ~ eǚ5;G:ξF!Uppi:`&FX8BK4KE mT3[5D%nCaGg"fΐEA=x:>ϡY0v hymg a:vJ,8+x2N0o 8ai"4~!EPn\qt=R -([~z\X~O}zgt[6Ddʘ?Q~mHD(u I$d[;X FԾ}{g )B%'uAyCx 36˄)gkM?4 HVjKd4=_#q0f]j;g}ytSybQR@*P>Vl{4~kq"OA6}b|ˎ5jEvc_j*{(G@aR-f0Kr}zrl$psG*? &md=dSfJ HBԪZ~{SMݭV($,A@f:ռRq0\זϮgH< D3ˎu }qf y"*u<4T0S~A[ݞ!cO yb#xEZǩ 9)W&4hu9).oT.j#5G5;Ir@q?p8IX?R)cϡi^X(6ZETL8[@6Gos+WP+~ 11וcA@R2B|:<Э oѕ o2MG#kāfjc8&^d~P?Ȧ(, y9rP&b^Y@ ߶_0,VvV8/ {Mجs8gUOOM$Sbg8 w&~D?L"`ؚ_&$6-o iþo0IF9U3$xk͂\~A={vZ̒\w 874Pe9+ FD46u{/ o qZ4\4iUJV[vߵAjV)7j&r4K1 'S0zQUը3Ϛ(`mh׭QQjʁK =V0=XFF٧L?h\OY<%pVjlb k֎ R4v-K4J#N} 63j?·ћ}L["N 9xq_ 8-!{+ǁx44Vw;=TVFevZxfqI%N̹e_87t?MYccCb$pc&,t~5wS4A4CM:mp<|Asܹ4s VH5`tHy a jvkȚ&Ds Z|rlړ}>p -dL&TK7,O۷R LB#窫qԩ!)~bm]qVf0 $ ybFB b A 6E:`1?M|!u+;yA;ǥ%9F/B mDX/f[8N,MГ6sPc=W3f̰mEgU}9/@JE@Wm#>J?7E3K'6YDuҧqPH[ei6_ď(tq2O| 0k3FT'|\95$0MZ*׬/e}z4+mzտٛdE yLa" 3o|&>DvHSGrdiFV )?\b&[XS8}q5^ϐx$֟-9οز$A;ewgUM_Ҫ8 ybk4I6!j'G.;\8(kpҰxp(uW[_ ݆/,$ Efs%2ˣ9cP%oIDATX Sx#Gq)BKMoƑ $5B|D~` ڀ_zzGPC;LC!(Znˡ}$'97n8D~u mƃH\DD"0tHE85HJ1*H ^r}5.1 S`*'FrDT#˘ ?ghj]+s9L[y=AO :.DGIDTD Hϐxg2 ?$6J[y&7:Oy׆kyLSQsD)A%]}|F n'P̶ 0˜ᯱ'PAV7#Mf:2fϞȝc6FbcNp]"%4fx簔VcL-=C   ?gISfZrdN&nB1\8-~9]g 3RWY*QS6|NAEQN]W2eHR:<}nݺ^:ZpsL[-Vbl͈Ulv8Ş\K14V6~r5EÈ|4ke͗Q̎/tEi IZF ) 7"u̽mov!Aܴ i6gVӡ?OCЗtEh A}+Fds20-kWO*N68hDwZ… Z7}#儿 ҥKݡF>N\Hp9"%܍Wi86qv%}ca~(;CDE$m&zgA`B/[ΉknH/5l&A@"Tɽa&yBz88"i5qC Aܴ 8LD$Z'ڹ#p \B1nI&\CFR3.?}=kVϨ7n;d2K#l:ir(|gS(cw;R`VA4eə3" L ,[& kaaaʤ ]Æ SV RJZnOٷ9pZWCb(#Р_9` !3}~7ghԩà+I ~vz{,*OTE6/lO 0muX]qY_NJ|0O4 Wo)A`Bܑ}P_cSP913þPEr97돿Ϩ!tDD-JL/psLit4GxC7veי#W6F9Dd*g^e? -RBJ,')Ю R9uUZlZX5͘I‘ #$.g]〝;v Bg9K|nEF4 HjG.koNVEk~kg03x0= ]&p؏/2C6tޗ}2K#HeXQi#1AP$~G\bm\A@t5d.   A@pHA@A@t5d.   A@pHA@A@t5d.   A@pH#p9t$!CRLc댍#FPԝ;sQ2ev%A@AyRJEg&⇧MPŝN344~eu֥f͚%8ꄧj/@ZWvlBo۴i+iРA ʺuF_{-yW'<Ou}؂l>~eI60=KN_4ih`QqޜKͽgH0.z+ R9/u_$y_OX}.^L2Pf_6$>gnzK,";ڗ}vzwoqQHK  )!p\tڵmFj޼W3P?޲'|Bh';s>CXsݭʒ<g=',| y3t32u ùBK8s{hƍ>w. Hs,Pٲe3uBA@{tE|%8w/Toݺ~'}Μ95v<_ח@ ._.KƴԬR^ch;c?/{׹S'W^}fs?… i$[@Tm bR*F g1oFUuDN_Cgo`T@~\SgDP!6C,H0+8s?j"iktm^g̜%rPPf,DGޓ>M s f0=`0gby2Q峏iu11Wy;! dΜ-^4yoϗ_~Y~wbS4 4} V?L0%4_& |o7@cvU@%&&fm<4d˪hN\z811}oܑ 7UǔɥYSKPq&TFnz+ZTG92#lZiJ /NQTt+_ d|tE+u~aCx#;Lt]vѻłY *_z۵/SLG.o@TbK&0(.  @۷c+awܩ75lؐJ(JOUYo!-$Y]4epjE5r$-X@nwc;gFvNX}L,sמ^|)] !"(]ǯШ y&G8= Rpt5jA_ûTt!mT9;Ѵ%4\iT ;?0H\Ey\h6 #.n3!z&pkJ E#`r-fKOnUŲ '޶~ }-:g iN\ۃg7մp]_kݴ."M'\t]ZWϟT#@כ֬Q5]E5śvXߘ?&pBwkf&mK+3Uu`fQNbI\'υ;&MR~G*e dz߼y3(Z,0^~a6gk[Nc}tW\V=|IiDA@A88hUhzZ/-Z>myk$p/q"se_9oA2bzЀ@3t#"I;D^EEQViKݮ6(bÿpYZf>9j*Xp un4lY2Q)u"AM!0%h3^LB]I,q&F2sW1Qit}C[,.w]_EA@?! i,)οa#A- ΞGټfeyr9Da7O8)y\G81yq‡Ӷ)ԩ^ao0e8ᙪ! ќ@3w~%t `βm5F@#HMƛv2AEBsHb={t4 faM\}rJP=(<`?7c=vPz5oA@A@/֯mР{&n֛osΙUvm6m}fiҤ$wf}nV BC-je9dLi)M\`_9z6lAƴh@żxÄQ_8@ DD{ݕ}܈. ;zQPAFp(Ղ_.r^zPyfUGo5iNǩ!>aYxoC++QYdHL5kFy8&GD[n4^nd1]~tlA@AO̟??Y??vӕLx@QfD4:b۠"}J b熼ysԺ˗/%ٙ5wM6 0-3/S 2FV } /LVΧT[B]MujE/7c[6!8LTFHmqKl|Mi 6I3Chb&9H xKoKhM_m r3=5g! 8ϙl8p~XwB T!s!qgKiT3C 2گM|GV59Bzgy?3Fu걒=ϛH Y!MA@:+Nݺu^,i;0&uE1?AK/QϞ=]$-Ƅhe˖M3G[odz>h۠Մ[ƌU;|20еF(ftlˑ/J}s\5Y%p9CKW݌ZўN;m1H:;AT>^xH,>"F" g{5K!BfL0Aߐڕ 8hI e =w>Z`|:}!pȏE Cm%4Ѯ?`^l{Ey0ߔOe  gCܟ=Nfӧ!T6\@:2hLnk*VH o -h -9w)o޼8gΜ.Ie:$ZDw9YuLMcB, S4+8t?.ӳO4g RtjL=`nq_Y3kgj65kz)ȹl>N]W 2eHRC8p{cDD][[KvHˠs!JCU l> Bxٵɳ_5h9،u'ٳynԙ_hӇ2gsmzc@?m߾^[-ÊUq/ 4q[ӝ:vT4TD]7nPro;gLgl*R%>p&޼A7FSV[o:lះYcYq.K.wt pq$L! !8l~؍(O$ƆtEFŨ0ĸ! dhh uI'. L!Xc,>ڂ~:D0ӆ@\@.s月Dǽ pr ‹ hEDA@80Ed9Eݺvks<@di휳a@-[FsMӧ),,L@5˗N뉴mۖ\Ixx8}'m6_כTLuY %*WLӾXt?X '*q8A1v<#2n=zqR" C8-rJϬߏ`gN#0}`جR^B7v~(9knjrMy[QtxϘ^ɴ҂(?#{_ 汄sy`hhʳ 92t&Ss 坟v;Xپv9]U]gbҮ -33c?1ʗ7;G\ɹan : Ѕ OӫqIu[mRI Y |^κs  "4ZW*|D|C0äNH:%R6ϛ)A/#dHrZ72\œ~h:A"RAI1+T@p` ?^E#P,{…)((tjG50ǞÉ*" IUz &(S&ZAlL9ڴii)k3uk*bz\+Y  pNW\!ƛ!g͚x}^hȒA@A@sMf6h`iʗ_<NA@A@HK5&ݻw)ԨQ^H "  BA@A@A@HKїG&'  @<Bⱐ=A@A@A@R4BR    x,dOA@A@}ydr   # . A@A@A E# .E_   BA@A@A@HKїG&'  @<Bⱐ=A@A@A@R4BR    x,dOA@A@}ydr   # . A@A@A E# .E_   BA@A@A@HKїG&'  @<D@IDATUEc閔SJJFARAPSJiƏ<:]vy?wΜs?sg=;tP@I%^xB#     ƍPpaɑ#G%{F$@$@$@$@$@$ ~s ȓ'dȘ8HHHHHH1/^k$@$@$@$@$@$@@$@$@$@$@$@\9Q& P      B.(vHHHHH(x @!@ANI$@$@$@$@$@pHHHHHH  '$      8^$@$@$@$@$@$AqoK9tvHHHHH" u[     7|y$@$@$@$@$@\:_- L > @F=y*492qn2I [ nD^{I4D=:Y+dz;wHyXzj`DM' aʒ0i@u"Z;ewSrt1}\ZRN`Kҿ@2l!/A`/U\>wY괩%thǏQ3eÒ? S%$IX+*;oK]ь 'ۗ]}O/r:Bz@woݕ >}~J.gǏ#W<Iƒ[/"UwoBwU|RH8om'd׎mI{Ϣ9A._vLv$Wܗg^[oE Kwi7%ׯ?ԭǏ]D/ƃOm?tu\WErw0wp?Ңlkf~RZ uP EVsn'+ڞCFW*f^SxJn\!UV{8q?o?IM$A O 5SO9탹^ۦ"X&zX'.ܳfex ~?PyD.I7u>Y6ߏ_?Q߲|CA7fN߾X:_׵m_@ L^HFץf7ٺz$MD+r*$A1ϭϲrf)~Wp>^4 e IRT؇³ʟ6 B{"E$ %f?kwtKW-%˾')ӥɖ[OV]-Z pCE!m*}pw呢*"ֵrdQmbz?Q|ѼǼ I3\{"=|bP 0 `!>'+)~qOtf}x!T6o|)K1W._e!{z_"*S}S4rֶ'e 8*I\I6Uݗ){RU^!.ޗhޒbKAƗEB˗~ٳw:Et8vCvuxJVWɞXS v ܣGe.3wҥ{j\Y I&cQD퍀]2iÐ(v7uUU“ysKL=mH.Zm_FHmvSѽSDjIG/%9KBI0 u눺.qJwu,.nX?p\kOg ݤYNN,?YpQFia,ԭ-ǭ+cݿI^AVa޸2h=0c ;m_jf˛aU|AɵK*tJ?!aǀ}y'}X/W;}j[ 'Vs?{t>}ZZ)#{g%x`ן_ k,(T˫fc!ğ2/p}XQ ^nK ~@[\\&*1u}xBw)VC?$& 8og~l mNa.9Hy/d}:кMu$&Ǐߔ-׮ْ|O8QLJHW橀AsUk&M-w%Sv|ܾ^W( ގ.3gT{)U+OƗx{!tj,xݼp Y^|CC7>*:ќ5(U0~/2yTj֎&sKS4~=9J%ֵTO</LxeϦ]8g̕>jg] s*1%ګ7njYnQTT9}1^Tgၛk$Hќ؍weG,u(Ǹ9g=cfz͑?'>l7Q ;v,ΘJ?l8 WoPx=}%DU[øL>mkKҽ-sʛyL.@N˪;f vm?wd+VNk)l۾˓'/uwHuۿ" Rs%淪k饁 `H(WYDxnu+d@o^?*:(|<-I?søAm .G:$pC0E8P=C}G4R͵xl"xjؘcoFpBYuH&Rc<: ^ya# ^JcxS0o0B+1v$뻯m?A/k^ <[Igae6r_Y!LM;jys?Îb]'Z$JHln7H`> s,pA?1F!8Ƹ57][ϟ..VYUU0lvW}Kƌ<8O=SB&K [*DL>_oPaw BaurEtx$?V!~;6z*fĆJ=R`y7FHDaTqu=~wloQ]}}NR6 #G=}@R%3o7#bf:yq筇:b^:]QSf`ܠ!\ʹ!;'kog臹Z= z Q\(|<+}j Wo5:t39fҎu;c.zuح l 8Oijq^? 8xF*Ch%a~Zqn׬ŻRx!TOւ @c:>oP><[VjOl6O[攮k~xam5#%K9F`˞ݗ ;;简ѣ7d쏻TcI+BBEv 8O3y^YeuǙgˏjl_1OЏWPEBWAmU{e~ou|=qoD/ }=WfPPN8aͳBcJ}TRMo^}3GF )0IM0. 6{ ˆC[ؼ (! Iɤ9 oUmTEs}~Qbg5 rG~bBtY `k tim[.qW7 jG/c`Q_«O(Bp m k5Dz;~qxi8xNAH5c@9Vd^/#S Z epFRZ͓ Kf~bC6ğ+9ϰ'K./kV[}.>x*6BXt&'/1ǥ*-x1  hcfpRC4+J'CBkPG$ DUB _Β'TS^*ekJo=stwBl-P_OPm0"=s.z }1K& œx\e ?_h @/wg5O,{] 7V`8O~^qC-Yy_Xᩀ>!!,j']zu]6TҸI@}p~IXnv]y0 mq7ݨOxK5 W>gc~a{?~t5͛TVS9VE!W=b{e.,& ]jP 4csYvlװq.Kh_\~5<-P. <p\g W$2"f>zx :\ݓ3[lE?ӫL/<ߦ͛ .b}s}7P_qjeoGWW[%*7{F 1oQoYWZ6l7x>i^M 7;A$7H# cꐎa=:@Gwkx_uw3[Ttfjlar1ɂw =VC'>g{8hf[Rnx IAsP狋uK:qxi.`@=2~< &LIZ Uyr?C0 c}b헧."[yLKCZe_\IF5JaiIs̉Hƌ3NaQx]`HztH? ~|,`~7 4GpA8Z]Y۳.=MT8e%UWv 8O3x3Ռ{1y7ϻ9Ə[y"༽ K<'¾I7k/[vm@RB#py%WI&[Wmә00 p7uD֤Q]F0^kzlC BKk=>{RuM!dtsVgTIm\I80v F藷\l]t`ҿ smNY6UE AoOU^wq?8u3J?{=OCPy}5BOeb舲n<ٷCHU1lS1'lƴzI=>Iq7 O-Fkk5y0# 8o/uuUa:ꩅC+`2*8&@WIj`n><`8 y:ܼwOxDxnuiãڽ; OpFfnsI^9j+nԱ~8wNԤߎ^ko>añFKF%jNxAQ%*c=uba^lԿ^zF$&,>@{ƭu˞ܯ͈jeE빋gu_ggvMpU+o]=93~@ Ə ˭zP/' R~HBƽj_\æed?IJfa՛},m_S^(qS?_9+ֱuAʅ >&a'7ӫw~B]Mgú1]*Xܰ.#%|/CCv]jvcp-*&ڄxKɇ!4wn=x"[2NgFPZopqI^ j[i ^BVa &~+ IP|=M+22 /ʙyrlRNf9FGw#ڰ CnκVD^7ߛw봩%-nf_9EI扼{m_!30_:d}qԳ?pw )]:D{3aaۡp7c  c`7aƔ'6SNZH3 !ň۴H5e3 #s%CdCF5ʃFyrpH6cĈ?{RS`~1wf 8l{E:1SWHD\uGAA9_̏m!h|8,?c|J&) Gc]Pӓ fۻuNUgu|诼e4{/⭑Uk1@Vε FiS$FzTz  1 Z< ,L&\SjL+&>Uhp/=nX} &g}/:]{/z"' /DY!`üόcߡZ5]St)䞺qLbpAZ 2,/!"+ajOq-$*^Y*Sm74L,[k\9)@B`8xXޔ$iLyܻDaz0oG?bI]2 cRqA(T"S +<|1>#5?R͟9}GG%_HS]M ;=}^:M<魼o 7yәSKMq`ɇv3Ãi["Mΐט0/SrMh=FY_](]Q'kog H҄$[Y x Fg篕,n^eqZb 5 S R[m앂0gF {:g'EƃB1#afğ4&6냉x0!ҰXm$ID{VV,"썘8lO^Lr Y :@jC,\掝v'XLy >R93o`?x /fSd굯sG)<0L/nH7Ƕ:*$}][.gϞ L/ J=^#b*{*^X ^2ߞJ1y׍J= uh#Ũ'og}?@MNfCg6o.1G;Ȉ}F^O7k^Q-|7[)Vhnyr}b㯾\/~ڙ,zt[+bVelҴ-$!*f0ퟏ=WS-ŕeM[MV:MϻoK ɮR]c{')[onE&|Z1xv4Ȟ#͕Xg|BoB/\FKO01:2m^|h cY%+VΤWXe(^SB'Vɕamu6ezq 2v-zsYbA/V2㜕Wף1@Iwwoe֡M ޯEVLΔlpr]|$7he^UB,%O=Vx@ /.gp o/mo\Ûgg8ԥɔƥwU,w$A,v>"Yj^-zy"$ ״3'c o1qU鳥S"̶o=*z D24w|D₈  a3MGZ<1>]x_{$t7AAt"aJ540A#]8j[MK $VC ys|#JFY3IxFHnC4/  x Axy0]I Xsg#/?ό_9~Yeގ5_!V ?TI|~aPþ @8#6c?$Ec \9o 4B *kg8;v^ $q;)^"ΓT{wy2i#*k0 W(`gHHH |0Io'~[Fi$BX$5 H0IFܸaF2 cH%%Nhw7}y$@$@$d{JY!hŤMߖ :~ 8@IL} 5VUr$d!͛/Ԭ=\"9R >HH" Lpmm30gGI ?6c.p5v# '"     + 8+. @8%@NO E$@$@$@$@$@VpV"\&     pJ.vHHHHH(DLA8~muYׂƫ0{$N8a2#   #@vC%5ãkM"E2{ MK4D5TY+d`o#)$v|v-z4I&/֯, &T'`IMѽ'gΕI5*T.$[/_Cgx + @:[Od}MzydΦ7.ߐ*Y8qP9 ;&K4 !0nV5W;q^.gsw1p\/dŇ&$ @x$@J}s+\/ODyn^xYs&yDIF-.K(5rז7螗ZJ }ORK)W/\-+[wV]-Z pC瀃 $@$@$@\8;!u9! W\MVRCW+շӖ˷Ju+WctXf4W~GUgLjHuewnޕ%{rpA]1 p>K./$n8/c\yx\ yDWO!oEˡ珞˵{C0Gl9fdߖgw2)T&}/Djוg;Ul?j⟗$eObDzO+s&frcMq߻}OYҠd˛XJU)!w( *˳'tS6=u6xV].cF>R^DseUrEu};$IX{-8^FÞpAyQ0_5؍G|+g!|RFe:bޞ#L$@$@$(|&yr(DOMO>hɔh9;wytՖľB}$ 1hqܚ b /og}%T6KR;Ͳve͵ Yft*R+.fCHR1F; %Swc$WWOm^ܼqeTњg Ȑƚv`Aۼ iՐ e~JxfuXU;_=X?kigd-&╊ًp>,MJ@Nm^-x\Di_8_$ϟ=wyNJ>-ˑG峒~p.hs|^ĈCf. &, q= @x @Y0N|sLSxSyK$~K}lK{"E$)J$X)ci ӗ\H6OڕWX\wJ,Y3[J$z"CP*=>=r'T޽r.,]ĩΝ K7u}(FkdIUF\QzJ/K!;^lٳogp.-LO\ fO}m@JELGZo#EE*'i2S:{"wI1>~zՁd󊿴8o)-%H@ /:x궭-m6ҟpf^|<&VoJ 'JP2(oǟ}$)ҦphX`_b?b\p ~ kp>Ca7AM*Dr]D'N|GĊ,סFN-RN!9۾cz:T 8t SuP?cmvW3=j;2혜Z|F/M1j^}x2%7x qS ?faH$ܯ' [_̢e[ku8@YZTX%f׬giҽ}ݘce2N3 8]?f_6b:]`{;OWF^J%owUkWFo,e b_/A8xֺꪅu! m:2eIHH;wĊx썷qoF@0빵!3Gl_q|VW̕R[aNbarw)ɋBDD &/VB娩5Ao,. zfcNX^C7*'Iѓ;LJ0do-14%nx"fW6Ai^M hovD%HXg^3$1uK*|HGuemu=[nh7nF9>J&--eӗ7lHx n5G8d?ʙ =;R].Zj+ē @x"?4ii7}SȪRe$j\[; j;:Oܦf4H`/՟׻wg;IKNq1]g &[WmTUǙ5fAa57']GtjMIDAT㋑l_Zf9Tb!{Gu_ ,Օi/]RuM!dtsVgTӲq&puN,FFz+--*f:D/~-ek|`4mjn:fT͹k- @h1cF@:uBlpQ_{A=+^!}wE{˒RUĶHd 5^ gdژk}BN?"jH-z5]:y㴪e,^ yﯽҦR{]f**b>V1O x=vVa#&4 7'-gdD?Z~0A fZF_{7+kT!ŨXo&0_4+R}H%/I+ 6o(1_=WM!a'q (,"  ?.]"SM#rszzSIO3HFx7%vdj6K^~Ce+oE{YU9:\xZ(djR?aXV޺zrfCh&F gf{D6i *^)竩'Ol߅+ MmsYiFY:#g0l`B`J|pXS!蜙7\x[=zFϞIedJŪvXN$@$@$@\D>{; E:}*%Ԙ)T|5IHHHHH40p扼v&57F$@$@$@$@$@$0p {K&     0p/]UjpHHHHHH;a"9" 4=9r)ZԻr+     x _d̙3%Cƌo0r: xG ܲKeuɓ'O9s$J([ L adܹvYf]HܹeB$@$@$@$@$@$>PpgΜ+/K%a„(Q"{Ś$@$@$@$@$@$@  HHHHHH|"@>nL$@$@$@$@$@aG.X%     O1  c͖HHHHHH'p>$@$@$@$@$@$v(Ž5["     P @ ;lHHHHH|"@>nL$@$@$@$@$@aG.X%     +W%JN1 @۳gO@ܹC%@$@$@$@$@$@$u(Yҧpc      'u۶ ~KlHHHHHH'l ĖIENDB`Proton-API-Bridge-1.0.0/testcase/integrationTestImage2.png000066400000000000000000012015551447740121100233350ustar00rootroot00000000000000PNG  IHDRB[h&| iCCPICC ProfileHP$PB Ez*6TWp-tUDVD- b .lo{̙sνs(s XLQ$ߛd./G LٿsUTPI^&'P}Kr@~EqnC&ADgS&yh&&b"( \I dgRx~fwyR3IM^&#gp%3C:UUr$ gY4't NDO1/0|O|mƜ)Nqyr9QS,bIVV͚bd4=ZOpSb8O3gs#crD!_ '{fwrksS{N/s{ ||c\oy-qF<^/Erzm ӸaS d*  W8w|#,0%5B_g2l-}BkӾ5$Eccc}A8I63@W6󤒼I[P4 t!0V87|A Q ,< 2e`(%`*nG1N 2nn@?x {0 A@TH҃!KbB/ E@P" ) Z@PT B@ UB3d&, 8 p>\o0_ݰ ~ Q@>b06$ ɈY#eH R mD !08 Xa0h YY4b01a7,ĺb98l v[ݏ=cp8: pKqq;q N\nk-P</Ɵw =@Vg ](QhLt%%č}Mb?qB2%HiUrR=魂BP@\^OdUMG7ϓR(%K@\<|T*Z+r++_)XJ ʔ+TR&*(++O)SQتdW9rU*^DWZWjRTu u84Z 6XR팚Mz}#.:K]N^K / FFFgMff&Z- pEZ.i ͠pQ<،ڰvR7Gttuu:;t. uttԣy {PcrFcX_[?@__ߡ?j`jmڠ!ɐilհpH(hQc18xqSX&M&M5L9u(fff5fwqLt,` GTJreLL53YXVyVuVt`M֯fJyVo666lڪڮm}cgadzcO_ilAˡǑֱ񫓳ĩi9ѹ cg^qxt95_nVnnܞ6-ov;׽]H!zx>2{`XYm%'?]}b_Uh '~~)~u~K`6pxZps rPdPE``IpK%99M %qiXvدg"# #E0,Z3/6COli,nVZ|BLs8hϿ@kAƂ3 rO&&J pG8IUI<6o;%ߋ?(p ݓKlILL-Kii>Hˈh$d&fEmYY:Ŗ",5{[$H?ʙӜKCR3<ʼb_X% uUzWV$h]ipeUU~[mt5kZ u ~HHRto?b~ر~ݎuߊJlJJ筿O?mHбiMMMw7{n>XR_ڷ%dKV-v̡lvvvYypyv|HlҮZWa'g.]uvGڿƤl/nogbv_Flu=}hc\'<<#>G %GQ$rXб'OT,n476ɚ;Ojmqk9N럮7F>}{G_ Q >{~zo֋/__g+W'ppk7j=]HȓG?xS࿔5-ۣ̱11WTd:?̝' xr'Q3>pU47#Q:5NCXxSC&gY?fͥLeXIfMM*>F(iNxBASCIIScreenshot9` pHYs%%IR$iTXtXML:com.adobe.xmp 834 2208 Screenshot (R&aiDOT(3%;ή@IDATx] M_d2c )EJ MH %!Byɐ!R((Sy@")2ݷ}9w~wg=|tY߽VB ];C?R%ԻwoU +-5A@A]lڴF3e4nܘԩۿ?u)!y)mڴtjڴ);w.^-snLA@A@A@A@dB@Ic0K!`HA@ ɓ'S|ԤIpBG [lT`AX"UP%K<`ڲeK|U\rԳgOŋiѢE񕵝Pl(DA@A@A@A@(B@I j*b_A@@iĉ_Ӹq㢝E̥ׯ_?*SWnJ 7oON:E͚5,0Jc     hJi-hRh*(|ƏJꂀ  8 йsge nRҥKN'x{WNc&?8mVdw !B@I%OA@A@A@A@A@OhK^  C s̔#G:v?~ݑӤPZJ}A@A@A@A@AD@(&"r-  @JA@A@A@A@A J5HA@A@B@Ij-*A@A@A@A@!ȵ   QF@(QTA@A@A@A@C@(1$R A@A@A@Hj%GA@A@A@A@Lb""ׂ   D!DPINA@A@A@A@b!\HA@A@A ! ֢RA@A@A@A@A@0\    ee@%9A@A@A@A@A@9sM"A@A@PZJ}A@A@A@A@AD@(&"r-  @JA@A@A@A@A 9Nɒ%۷ӵkb0)    77#"    @RD ( 4iЁIs   @dȐKϟ={@5     @PfJٳgWg+VPn(uA@A@~R@E?z(?~-nwCIA@A@A@A@A bbrwF\Ν;Ru.        J7Pbx        @B@N o )        %Pb|        P^zCҖ-[nKA@A@A@A@A@A@A@A JPbx        .xX@q*   h'#ʓ3%ʛrdIA[@:oQA@A@A@A  ( |ي`x  7"C;OeQU4'KptA1sJIWV^h\ű$|Cx/1V~*K>kMPej'J65>}hϝ;+M1:HrXJH,FC@1o9sV_x䦁_YJr"8}*[oC.oT6lHEU՟6m7*a׻KC}';h=wPl)|3ԬP|)R, քo)Sy'Ϝ9CG #yDb= RJr%,Oða4lܰիD }^wc1TZ}u/R:Rƍ[D{ݧO#DYUVo)])OlvݦО=(iߒ.3__FK|7^7cme+Fj={ji~ޜ;BM_|%`z[71`._*%ut7*U |>gEN,?`""փpPT <4i;C=w5n8"3ժU^~74hPL+B̝;4k֌N ]F#̛7ҧW^JMsR+(;M '<ӴvV:Jp?$`ՊHvܹs+".ٶme}-G  j܃rv \SXDrzNb!nr܍nW-ANpI7|3ϟ_ Xɗ/M> I$.6m!DNILA H4X?h{ϵ+ tLg}FJ@\,l@y$[CSF9Waǵ{Jzu%Yؗ^A7xB-jX#d!'P0^fKS4(N[B˗s/)*Q5Qʔ)!sh"W(E ݖ̻&ZfB8=&.]Z|z^qZ_8q0U{']tY99݀?8wޚV{*H8քnW;N0 (kZW_B@yv 8oX}m`\>z]  )N~m%J5Y凒_~*M6-ZUPɓ ! 倜e̙ YG.]8*ҦNN?YVPVP%, j^ՠͿ{R/q S3zl&1_9G;J D|`-f+Zfi|<,b@"$D8 %ic|B(xd/^ܶ wސ  ,! z@(&O  pYZ}?o<;cƸ+E(/5m߄DJ7A67,L>PޘXn/ҧOcv,LO= UPN ` o緝P":'`[-83x7^N@ۖ*uh^ڽQ# V! NÊIN@inPU+Gm1-ZԆX‰*3D7]|}C n @~4R*CrAӂ{ $~0!j @=JcPP0A@Ă@!d*Q2\zO+ S(|b?6/gT)UclpT9oZ[9IZQNϪ[~MZRu^j916xp3p՗t3ի[2AJ8p(? eٳ{ǏK?Fz>x]ʳbhUwj (^ G7bnjCmWȜ923C}v|-ֵ֮35ӣV?̛79紪۬+h=i <~02AY:wDFv|VQY(Yyo߬tyJ] 1fZ w" ?VV lЮ]*{_1{B?~6&LX4 $̡{to`OYެHb1l=}z1?ή'7O!ۘLY͖O`:`+*=цТC5pdw;/hPPQ#[۸|~/6m) xj( eV8»V/-c;oQ%\9r+MYupaX|¸Ғ?:tZB#;7GM\I>zN5kVz|1^sέb,e!kYp3O}zg.h@8z3:?={x}/qnh!~wӷVP0@`LNnzx9q  ,mơ`}^Tnܨ?MNs9s~ߧS} pG+>M2p 3Snd7BPcǎrdȐAp?MPfLPf`c…գ[ĭ3gVJ  Y q6>ѷnI^b z{nuy0/hÄKٲeSM)#d={b.ꄹ} D# ()X ƇY3 ^Gԏxh/цܨAsӘ;=^D ] " <ނ1scyGe. U|mE~CIhW0@Ϟ=csxQxW$x]:9HDA |b̳`kBOyT_J6 ;z]>{>.r<Ԍ|L;B:8<-ey׮^Ck'Х9溃{}xh$Ejy\OO3! HDNl㭏o8>k?۹󀪇$z}rZSۮHW]&[d06̺ę/ S#Zۖ>d=hiu<צ@K|΃ Ms'^=Z?= fs8>żѿ@B1BߏotrFݮ  1h!z̘:.7@2\<^ Xo#P H5ڲ˹sF>ezKYVHp:O~'pP kAU2<9uzOk6/.J2z%{O> ;nj7M&/[P( zTVٳg[6WGaÆ)_~ ݻSxbAގVApqYh8RuMq Nef5￧W^y=1#=tmӧ(qr  Òb!=}W⤼-/=S^\C*A7;ZoX܀Ҫ`:X%P6@qCJ(5Me(k::EY" J`fp]AQh*8P>qr3 *̸\M` O%7R!(TV~Mxh{RJ?k1;)Ÿ?qs=(siDxID^6q;0(9YYXO_hŦz㞛2:v55PFcN\H-hP&]iǎ-O r w+yX7jy˭_ye E̵sKz)<1u0S (XArk_=t91>7 o`m x7bMh{'P SsX5`v}9A@@Pl (!%p (QX@YUcNE'~t!J. ,h9wp :UrYKxaV|\Cu W(c>ekҥKi o_֩.aJUW’Sg^GN@7ǁ㍓tqqtK@^A9Xa^XA!GRA^pPOjf'kJ:qZ/LB!ւ:Ϣz\ ^p3q?,+9|^KH+\ ,͘^;RNi⋀IZP{یWGKX$N nWF;lYjȗ <!Z);"wq=N)`8`(ť)\ (WX%@: k5G1vY *(- s(ʂ"#,2A4sw'e@ZQv:)Ρ1s<#-3 >̭ ! U'uXK<};NuNmfZS;ݝoݺ3> N+tR_qԾë߿+? s{O^/ᶥ)FGQ7t[P-p2mڎW'yMMr#Z͜Rޝ/>ǣθ>DvF.xgRZ[ |j=nGr$0N 8ɒT5X \:^]1;u:I')XXWOؠI毡sێPP$­ѩD/k׮@;\ߵߺ89+0ϭXQ9̱,0+oqv?jʽ\ T>SuB+:`s_Cu"D]#pF0/C`gwdMO:U} F6lI*N%KTaSx^+L-2~_lY[W%^R&-7@)S$S2x[2>Ŷ( `N\38h*9iq\⦼Jۮbu3aww…:Y.;=gR h aB CK`mAJ!'4?h{4SXS17C:fY@" fpCk ^[I2 ((kN}X"i$/ <('6GrRcT^ܺu%:XZ*S- 7L%0Fڮ:hїE5:: 7,(&Aδ&CBWN$|(n >b;i 7sN9D)0>'Y|rz?\nyoa1-S0c?/$yzM&nҜ/` T@~2ŜO@BsM71 f5WP-DqR-Ob xd<7.nXW _+xr. B@ KIjd7TX3.{sG)sIχke}+ՋλEε+H|Y-Mco=JnHZwuaZp#8&D 'yhEn₀RcLBUN|8< (Miժ=WCrJnf\y rgfKs~+qF@>%=bRN{xv+WJ_b:T (G fwjB9-4o1zq?dV`!xu (K@ᮆ`noDҮ pP1  Wۘxܳcx_Xw9ut_ :L dY$ ܹZjS2u ꫋݊,;UEF@\ ۤw eYL,X$  ȅধUh` ( ꤥGie" -]NvtƕpuVbScs(W̳|8Ʃܤ?޾PUܯ\ㅏӟ|<܇ZLw:•\%Zu ur\xZ N@^vi;9_Sa]9Yó^e)CR&M_Qp# 2^tJb7ؓ cg޽-Ҥ]e"DXG|DM@~ArRC@>2`2(7nQ3fػ%,X`[u?ԋX!=reɫܕ @Yo,&9b r<Ԩ=]z[_^tiSoQ料)T._l\0y d(e~Zy!/PYMB+P!]y{qk :}#mV\sK-5 񠟛"e*Ѯf\s҂S:ntjX\P8!*DO~BW|_~'ipLF@ 4?w[4Ǽ'ca>h\⻍[e1 vy>s NC↩jkNZL ''Xj1³jӻx]9  PB ~)7>h~ Q~6LJm(u[swma6(L,D%6}!@1 |FX^JP:wLӧ\yVǿ.T[;<ޚѭܫ@ѣ5i:-OY@ihQ*$K 07 3nBۢ ʾ v'GOX-< 3@1-vp˚US= 5Ip%v8֨Ryv fŌ|߆ciLwa+Tmvy=I.vm LC3fmI)~8Iԩ:8%.\hԮ_"$(m ٮ];uX `7E{GCBݠ{饗j)cY# >.7hOΝk[THlsPF_Gza}9:kɎ:X(ZӿSQ@'$pPtp4n@b%]Z!wk|va70DFzn7O '2[77 W97x:N\Y$\A׈uNq_'e`0[p,6A9Kx]L,&ֺ|4hN;GX3N7AqA@evȡ㕓Ge(Nh'ih<|.0S|-Iħhߘ󁮟I* F0GN0-|0sCn@yDj gb>^YkL ,{%UXA@ns(fys1vtfO~?ݔfv7 q @P0Tٿvc<_ؽmfR^C q$Wf/>ns'*O9;L>9L҅.:ֳ>bLJPkiqjRMuy1j%Gt"pb~h%)X0,s- (" 2[ q[b!Q͍e3\ (ʉi%v4eޣ aSD%[3>Cu>Ovzv4p,`(o q7)&BMuLG鴖2-ov[iAHl挞qK}y+VrsPTsʑ#\/_v l1^RV0&D")8m4 bӦM(ճI+hm '$6JE]χ]/RUBoʉWO1eO$ w:.nx's^R a*hgb&̨&*|9 ?*rI/`ZşRȻ=kZJp\A3 Sa (ipe\]} :^0HD#\B-8m⮭N s<fbvyEhZ0t"%q½ q]\ ^n̫>>44NF@\1eaEIPȟ] 6Iv F`es('z o gb6?'5//[s=tU5Q<ŋ".p$ duX7 ͝+ڰ/Q%.r nV2֘mL*Y' wXؘJu]Vtts1]߼6Tq~냛xrJ^"[yò r$Punjc)"ʉO8Ұaejh߾# pqjh]\N@yw:=" /TVSpIv" W3&iA=tWAf߽z*Q6]`!ЅiܒN 8=Z+^,/ (Z%r7 x Rx!xҨPquɭ5-.'6nx- 7q+pHZ_K*,<ˍ{WjaJR D]1^9 N@A~*3=V{-SBqcqRHa^H (EoIC+_U%y5*V=q~Zli1G{O:֬~_Ϙp(HC'ZdcP^Ξ=u.vmܸD'@@=e_@IDATgLe)QNRJ**}яyHIlqӏzv[=ôWEpqGtg-MTTz/k(^J/bHq,j`SI[6. }~J7DHjS1}9}78ue@i(v B vc6t`wJcx P: nZJ|9u7 *(qyǔHL+k(w`$65gEhWW&aL/ @D@fnic#\)]|pE# u<؏O (јy<~#j"9;˃P5ywj+fý@fAyw qc 8 ɸNH 3B@ @T_J6B/lJ6Iv$v+ 7W%Gq{ F|EDu9]|'嫙P8 ܴ $ nJWrLӅ,M9tp!,Q,7v0u^ (w)nAxPsE@38#DcK@I?1BP5`dժ ̡H뚏= w9s ZA/R& )HPѮPK8}\{.wks}GXZQNT0'OvBy>;Y<$-/_޲޲PTxsmC@5kmy@ F@0A;>MU$PtB*TPUA1 2JR2wd!*nvHL|]sGy>S{P (\an$r ;,wf܂W:H1kӥw*'IG<3+Sa},p+K"}/#W9S]X嵖H ( Nr# pF*k| F@L =M9,g*,Ѯf~^]gtx&I40o D# m12s E@\_x`7~ϱ&Qw  ȃ`ޣyDH\PX\돀H^cv3 q]%7|#A@ (iGy~v!W%!krlcԃe=S,Pe7Jw]L 9w=E=XeLј!$DoӎJZ)%5X o;7eJ*Q_HCūys誕cT~f ~s w͛(hhWn8}#Є} s:q\2`MڣK>Ýu)8Jٵ|]Ww :_~G6 PJ6lYCx851Q vc:0~0ͯǎ3q!1B)t h8X!Dxggي+X (>,5h@СC\TѣG+\p/P6,rdya~`Ҧ}Y.;Ng$9y6VwS#>W, ~i F aґsP.ӭ[YG1{)*G Bݠւ4qfs9T~<߽eqT.k2!R W;aw#~.#W. FAyj Wަ%(0SvyEW|e,U? <Y+^x,* ޶ 1^AFs#_ 9>J4r/0gpk1ErF|͍]es34wM",EO2}_ HBOA#o>"WVyO/( cލHÕ&JS~MTz\$p+(<wq"DJ@yoPǻ ۘx;i NC@EB;M~Ri_4fmևے~}_@e ,l|*h[b Gč J蝷"y$Ey wم9sjUe8K* ]+I@FhkgGi']0,\協,l=P'˩37A u}{@Ti6a,l( p+w0?: 0 IEJB+W`(S/i[OϾY|k& Gc#k02q{R rSr1᠟EĄ <#ԳgOꫯW_}Es̱%R;a'PVEAL ܷ|>r L! N`WwYpx4 ( Alb;w<'bo"ݜ>}e,6lGǀ0c'HBssg #4՛@XP.pegM4!z@1լY8Ecu P~:|S8 7?UyWO+,qZ4P (Xá.j? !Tp@W"38r6 s… StI u s]Ǐ#W"}S)ĕ*K.w@N)??" Tqq٭`(HK6q=x jǻn雊z^pթ܉1ThX:yk*+w`y \έjܢS<3pq6|9|õzuPssDJ@(X;lRп $glr0[ecI_zrO$M@Y ~jc,&{$OL֪GPnӟԭT;I@Y[}}•k}KFo99}:1|<$+7%.w`UF[ȝ;E(M <_X?.~4i:ڽsT_$a;9y}@^JH (3>4zv;?A9x$"@2N ¦byDڮHczT 8>|RJgmG[:}2zv" nQ&s"eԻ ;Hvꟈء}]SI@o+VMfFZCNhYsP1^!4h𸲴 ޹dםU{zt^]̉g?Is+[Qr%TNAfmnDrrOvV>V1 O6yK W/bM:t5=wFk\4Nkσ! .'"m3u2^'7weZ4~$ >ʮeȑyf}?i$;,e…^;yhٲevDRd:Z8܅twd8/wCgQ~_j=xo@ Jpe+: .P:i҅T:qKOǕ2K TZP\94`u ipJD] \am6Pn3_|wL*J8.kM%#%\ E6Np(_9{\F░I@M- sF]nR>YD C;nyNpBWNHjN w~ȓ'nc\dzۚcyECѥdOs28TVO&/3f.?}"%plvB{rvz? f}n~Pp'+?hWSaVƍ{qxpȷnm\?(e ;ND+nE7wYʳZ9nּ* iӧU~JԽNyc jٳ,gG5"% H+G@jЗ{oGT%;vVK]`y#Y'h{R^%K>|;hEkh5>"K/դ5k6Zd r4 ˲/qHW7mB}I`p\@MyP]5^!PF;̓~G#F̷7=&AP '?ymEei1C'<Ǚ3zX;=8/ߡ]z,/D! W5ʦMބ 2rR,"!N@a'O{_Qqܹqшs]2_rJ?^7B!N:9Eƍg+Rׯ_O'h?pd3 #`6( ,@{$!$- D r ܢ>`V8)I#''µpBshM% ZL^:~G$({y<]N6*pE" o@b qv40=zr[xNtD) V^guS|<<N@h0O Hح-c0'h1ۄ+usrLasvE"<4]`}ԏyPz&em8Dō4&"M`&IʜPs;+0xᶆb| .t[z<|-@^<\;hWN48?iZL9<N Z8qBx>疊DǑ @p$iJQΊl$Fgv~Cɒ>@%(#sts.:D@ ܔ:#Z20%O4p|K3_ wل5{{Y^A9eEo-K[Vm+P6VKgx,Q0Mž۪Lꪥ?cA^J=VihT"T|I,lzG-4Ξ=Ov돀O dɒQ(t/ۥK-?|\J[2ڳN КvRu*V6>SdkR w|0u_*L w}5IG~}ͭgd)yx"mv~RB)O au߿ҥ-7">RLǃ%K]wݥ>4x`[M tp%L{^o,X` M^Y/Z䙚5k;wT QS@RsY ҧWAx;S uDic1cp<w::e2UvνeM/N3Ԭ-uLE0 S:9%kvSJ)sYd]X%p%j*YtXr) x@i%8O(g\&D͐7ʀ6 (pq"\ +t<( EC#/$': @Qf*ЂtB}8)A@VE( w.D@F<97* (ht;a-" o\i괓$\+HyIPh#F@A_ |B܀9]c8ɉ9)71x̭Dc. "{i{9i =Y>C@vԂkk, 3\'$!mXi}cPVX"/'M[p18 ú6 z'GA@g4U3mf_JNsqI@u549 w.= (p0ibg[AVF@ K}6))]|<w9á$4iR #|O^M ^f%ŋ% @nGXxuBG=tw>hȐ7}nC;uJWp{FTSpi -7qW;Y[cXi޼:5jXYGY>  7p'sZQ:#a,[mǏ]~V*@( p+Jԩt鮿||/lu$.yײ͞="x.qz)W x(<9^ڵٜ }c>Bu||(KSMfk&EԩC7vcl׮]6)'P(w=/ұqؼn62JyۮT8EOL唩xq+ &PBӵ ϕ)<\C1*8)0t<aT[N|7q0brTuDNi;v(H4!tL;`[{} UNG'H* r x8G|_8%$8=˸D@Di3 _vgbqVh Uh#̈́\w< XЄpND},iLZ9r8H$ڈ (Hi:bHr9N秏|C?3A@kS9"% @k$(H۴00-' :/DeϺhmrKA@|H<)S'T NnNշzɭ(#(EV;t7梏P<&oЙ}DPtYT?AWΝ&GHe- %K@|1g`Yʇ>4= Α y`9EرS2 u0>kGHח֮IߧogZqn,>B)Pz݈wɶm{GLrnEIwF>Xd'<6}r >Cy+K$: \'O6څU㙩E+^p ӳGCuBR[p+n x7FXeeTU+D "?k̙]0" ]Pe&p);*jg, 63NdӲf} ڱs?͜7&r:v?LH>x' RefmT\4@-,RMnRJIC;ŧߵ0Y3UZTG+,lm`%I ʭ\h#< `lC71˜\׼缟:t5(+W.-ăv'G#G:{$MKCЏ0w>9 m o,a9UvLs(cD%5ru.^92+ ,5#fW>3xt<խ؉:w9e1G),yZcٿ5i #$>5( o$ګ"X;H`M\ѻtw7ysITC4^sLg¢%M*VeGX/eK߇EC8= vC&E! ,|)y?vL~XhOڴicZ{)mx_ZsLhKTvt|mڴϣSLQ}n 0`[RJ{ ݻϓx;k,V܄-ZVFB* 0n]D8 *ZJ_ulԨvUqse E.By~v>T(G5n2EW+<$E \ J e1P,yk~(2P~@A[FGdAަ2;jaf?%MQPWvb JC7aO-by8Kz糾Ǐ @PSva]}ȼ1# 5kBd[8A.m'A(_KX}'c4De?= e:0Ix8ptII [up>'brWrs5(ᦡKtXc ȕ'\s &:o9-K{n܏t.:Oq``MKΗa2a2.N@qs9ÉBX@0Ŵ>5z`Dk]z/n4} n3L̴`Vg= LIDA 2 %jRʛsRlA~+_#'LKҵҩtrsx Ծ1 {[,w/p+}S(n5RCk!d2>ѨR <&\ dZH~x_32e lڴE)y/^a^-jGƍ+?<}{JN-*)mA U ֊MIF,lc7ߜ"Hɟ֏%{@+]fKX3VFKh E~P!އ% X(n9w5+-ԴCJ>7JXiX}>0R (VrF2J;:oEB |pܱ#eWNeA7c;2n8iÆ VLݓ('g3mET9YsNJKC*y "  `J ((<RSQ(&Lqbu&N,?hO:P 0P"-B]DH } ϣAiHxPG0ϡ@FAe hI0eurz7Q?+iOb]1>]#\cBHR [,[8s90·D)XP 3q-?)܇94!@[b.N D^h=o!mLtJ# pc"7DM+ .\XY22p^" !x %6H(.R*A@A@A 7orq;wTJ,<(Z(eϞ.ѣ-Wk9ċ\iFB@l!)S& o>U(O !Žo.,`\ȉ @PdW\ " H UHSOA Q" o6!f %6EJ%  $/kҌ3E{ Hń*P&>Qd\&3k7-{bZS}k#-,TրXN P 0%?0DW {Z9UQ(@ P"PjU,YiҤH~)OׯZ8 (@ P/E&xY}5N/}}}izQ@ '۞ß?K=b擰](Nnۄ @͋)@ P(@ D5%2k(@ P(@ P@x0%%\"$n݊ī(@ P(@ P(@ P(@ PH.`J$@\(@ P(@ P(@ P(@ P @D'Q(@ P(@ P(@ P( (=H< P(@ P(@ P(@ P"_ZjC P(@ P(@ P(@ P("]Dw H P(@ P(@ P(@ Pt @a(@ P(@ P(@ P(@ Pvn(@ P(@ P(@ P(@ P:0xJ P(@ P(@ P(@ P(`Pl7c P(@ P(@ P(@ P(@Pt<(@ P(@ P(@ P(@ P]((@ P aRs+6؊(?C-?#K'(@ P(@ P(@ PA 0%HVR!6{#O`<005+g@RUPOVoYsG=P WL36^-uaiRˠ[ўjref,(@ P(@ P(@ P   G''}gXB 'c]fكxx"]>+A``d5{< U#wrh c@b-Xxj^@ ̭$h~m~ND P(@ P(@ P/ 0.`Ԗ[ ?[mv"["mh @("$*ݛX"vaQPo; 2A"G<I]GСC'O~ɒ%ؼys YP6]3*R(@ P(@ P( @ Ę2T80ri܏apQe\g"c3%b\TP|vNٌU{+v#Pa #[Va/Cu~Nĉ-{#Vo2klSPs gM3b̙SY|9l/;98aeup(4^@nd@@ P(@ P(@ P@ @ 'zVF/w\g^riV_6ʔًEmagmծyB&Pa8M$4PĄ݊Ȅ"iN9P(@ P(@ P(z @ =(3RT2: s`JyDϼk"wt x9F+os D) +뭕>u%ЦZ(9ˆrcܗ^=lZS(@ P(@ P(P7t-ėu{F9$)]bClM?'mnIG1$U:y.=#?Nو=+~ӊ2.:ʜjwo_kF u\8k:RWnيC;.bĈOY6bFPZCLb)3^m$ܿr=Zxi M:#W*pOAn3w/!GyfpO&c-K޿}G¶3kQe~MP{x6;OVPg?}Ūq=h4˖qgرmֵLE*s*[٩}2,.?\n9&ny(2:+I>yyL~׌ |LttrB>.G~*s彼zˇ–9c͍οo+וe;NQ>QM͚G4s.mW\"d Z'NaٺZ4zg"мјš KpRi;~5m-F͕žcnMuh~^-Nm۶F͛SNESZH,!btw>֭غv :{G֬is&MRÇ0cz\pی!I7LQ'*ѩR%O{˗2*W{TXX-Z 7EdGe_ʕƟ֭d ö|3gm{ԫWRo+1nr<}l|ӂ<)ah gi6m O _=y)'F՟"řX-A \lD~|n\J_-낾\. ϛ}k답{s\rz\GӟzQ*_苍ktⵚi_bUB|߿RVKU־?JmeWƎE,7-("pC;|{k_R/D`PP`=sz6F! K}ۆ6Ďj!ָ`@+>oB3<}f8nj1K]~aVj|d,?sfזFc$Et5 =^|3c`;[3QzQjCaACCkS^jGbZjfmBj6 uAVz,X6l0*C4G֟r廫f?u;t=Xx̙`o ԩkkh;nl{.Mv,XMVBUd3ѫ,63ftGle٥wsFŧOѳL\tǴyVGY5XunQӧ+kZՑ+aڵ>Ai$NiRU<T$2Ċ_~amezɖ[5+͸ndGYU(Jh}gygl-b>w()5 MTkRFB\ 33.u"fp,Bzd)\JtG{yn[+_ JfT ufGuR{yM2M`ㅭ(ٳ>|XڱҮ:*ÆԪ?ʕJ@IDATH<RH -8#A#}J&NZ;fRPLm}iuȀlNRv:Ǐp#čsU'QԶ+OEƄx8iʀ"&]WP^ι"le6? (@ P(@ P(@ P_(ۅkO刍'@/h5ZX85DKfdӲeC^6Dz 搖KCM[|8} /&6L#gA41݃uSu o^axxQhl{!a;/cd"Fmą>Ea嘞v?bLݠ<0lսȘ ?{5c3y  B^L" .ݫxp7e[A͆Ddc~:"±[l?i23b mEƈPx.KvٲyֶjE/Т8 q適Shv_;wZԂ?.A e{_ #ys˗/W6!Io'ݴ )@ P(@ P(@ P|^xv۫mG|<~1%kY3><{Vl1j.-+/#~* ePA2H:Z[^PY=DFX4ҝ8VGվ?ZOè>el/>RGՋ^ kA*]Nٛ>n7jۼXZIo9d-:x([Q7n%D\o?V;,6jcB`-𨪒t6{P[+~Gm>gbs EƬ\d56 a5 {$[#CRIgThMtQ},gu>aKJ"eb" RLgiK-m%VWurmkJb0h [^rzٶ]C,Y"Y@g?LRˢ[mj|ڑHU^K٢NP7}˃j^=C!LT1ګ^e4VH.B3JhS{1gu (@ P(@ P(@ P @17%=mErrma^_Ff;qO.Ep̜?jHZ(Yװ›0~0Y4#1e%<h}1JSzq1a5-oLfv\V!dqqj&6A)D񥷥b(`t@]^U\;qݾCovEz>t-ſm%ډ@JfqW{>"j6~2UxtNd5CN8[ݻ|VfM2j\43*Ֆٮ\zcbjP9П"vA۲g^fFyN x-X2̾/>9l=/Pڵkgmܹ8uQ"n\vqAb~A;<=S&=GjnVs899=~ƿ͛WFȒ2t5x{?Ec&.\]kUz[݆g}Qρ? @)z-“׾@(3P(@ P(@ P( E#N v|7k۴س|{6=z/ڡ|mHï[5LV/#k Ka; wbvUlR`d_\y>ptkvިX @;*(/E9A=D¨-gKX P*-8t[ كؼ|mC5;bKC'nbr+×ʦ `Cbo!b[d \Y:Fx;Qgu=yȮ#t&u|g֯35޳ƛgos俘ϐIڠj~rYA}}&B+{qgډCJR mHϼEnRym)',?"':BZ;Y=NCLObtmL~5m]d @gmCfۭmۏgF r+caC 1n~'[9R(@ P(@ P(`&3Y 22t|miVZ1eE:2_70lw,\l`˚Knjh% ϟ>b`_/1F>K2/:K_Xˊ ~x*VrUHD.򗫉 V~^*PvX?u0n[c43׷|&7dnسtv-U_W:)[kCl$ag{֚PϹyiiʏ؏~ֶLX85%M2st[6!s{dF3h؅8 ijxo߾Ág1uZɕ+OǨ2/ߠn![D?le6KF!aj⤕عfX\g4봥|ޚgQW(EwAd# K7ju! ~y,vfWS:$4PE|=^y&F\"3Z-씣&,?&1cDl?oP:z5@y+!i}Ѡ0ߕQdn J^K Yzkb{}N[,(W¢E,Q2g&[v3F +U(-A,5t\\UV55(@ P(@ P(@ PQ(6ETs}y}^=_FNVbm;Xn"2}DˌH-\`ݔ6-Ș;Gdפ DRa}r/_m|.!kD{t ;S7i-I2 Z}P6."Md>4>G3MjQQ6Yj;tKXVm6G |AVAHfl|D{K ItrZkkZ^}&F. @s, POr $}pɻ7VniX~^& Â%J`~˹z*z^kTIPd(Y5V4+HĮXk&r2SvCϜ^ g˝aĕj!)~ѣՋE{dzF NmTB(c+NDY1#!^R(@ P(@ P(P7-[mػez#|SAŻF崭h.SY2]⹳3ʖ-kTwܽ{ר,,.jˡ\_:h4#7_N ;9DŽPKߚEĞ꒿u Z4ID7(b]#fKm$To$^3z*E& _#%i h:%mXMҮ'>s{P%O #ـA?*(.>{}"ݻ1g[kmV@bK q<Zku<(@ P(@ P(@ P t:a>{j/cĈ!( Vץe鰎phն# GrH|;A %C yuH=<F+ݍ#*@,BҌtRb"Q־:1Zج̂qh2N%hNȐ]G''R'`KDJijj/m_]'"sM1`e✞?Al bH#;[̆yH-,{ fu<(}I/^~-!n[;fV7nڵ ˞={ T4l(k_\[ib P(@ P(@ P`rS\umf4q|Zʦb{ӧu[l4N{O~%KʽdC@SHPDۃaH ~jT>۶[CZ|-":,NͷZa,Y0v 9sUbݺufZLǛ$mXDF(@ P(@ P(@ PB3LGYDΠ& .E%tItż_3eL}R;ȦgmÊLC yJmܿWΑX*yСC/ZAX/;L[8n֖#˭m$2t ~5B"we-gq#Jφ_YirS 0]Ï Զ쐳x4:CۖE?;m]Wq0J@-׿ M3FB3E?\xr+6<=Ǡ_3Hcz|:5CmEC+1V!{Z+tVWKs,w"(IRv/ӻ xzœٽ9ulڣPlfjۏP'۳ƍ3̫sΎ18p'4(("ѰaC%ۋ!R<1TYu4Fmo,(@ P(@ P(@ P  @ UiYFe˕)fr VWFnQȦlc:ֵ1K~˙7ϟ*[DP '/XG);t뻢K.x0Mm!.eχ#f[ xt*P@l"ClI$U:rqcG {2Ykf0f㈂Ӳ "SC;D?eYڹ_(RbWy"3fIvPR]Vcz-zoP>a!hoY|߼|fzF׃VDPߨ m,rMԬ6glV?WY+3C4hMݛʶN-j]{UkFPuOy}&J A޽y|Al?gٌ ~[In#F=?e56bNɴq-P XdwԲvIX}^Z]͚5Ѯ];6m2*=zG/!{:@¿>GÇg 1m#﷞=J~>+3̘b ȖA r+V4Z?G^1aț7V&N=߰o޼CVUe 3#{~-XҥM.%۱|.(&yV:/b}1mPlYԫWO"n\ʀRCP0U!9dq5yM P(@ P(@ P@(0%#jn(f#Mܸ$4o!Y2CpXŋ2}&bE|Pr{"eȐEW@vH~"Cg"A2"Glr C_)<C e/^>Xϖ0 @R#dAJ9W x s]K%gcoe+x};4i %O%Ty_픮8æ .ڵH)aJf.OJ*͛Mw )[ܽK@l.9p;$Pݤlm7ǏϧNc.ܹϘ k-E,Ul}vS 03ib3ea!v(@ P(@ P(@ (K.(S\~Ҷm[|>ۻDs`"`)@h-(7G P(@ P(@ P` "PV-/^OV\!ऑO " @|?5g(@ P(@ P(@ DGDǧ{V}_=(@ DE!ChrS޽'mp(@ P(@ P(@ DP"R(@ P(@ P(@ P(@"$y(@ P(@ P(@ P(@ P A(@ P(@ P(@ P(@ DD'(@ P(@ P(@ P(@ P$(@ P(@ P(@ P(@ P 0%(@ P(@ P(@ P(@ P@ 0%9-(@ P(@ P(@ P(@ P. @.OA P(@ P(@ P(@ P"H(i)@ P(@ P(@ P(@ P@t`Jty P(@ P(@ P(@ P(A @ xNK P ppcpx"?E)@ P(@ P(@ P(@;bP"Я@HU>F zg"ir{ ;3A {~ y }xUJ{!g )/o xA P(@ P(@ P(@ 7xa~H"?~ϟR37\]㘭)0 4eV΂Ȗ$;FW/'vO,8 qptB/$HpcV<^TN.nͨIQk"Ԕm}._X5r!gy0(@ P(@ P(@ P@$`J$~8}iŋBze.]2ċ1bĐK(w`=طm1NNf墠\Y1V )Bj6-"A(x4c2gw4+[uvM=ɶ]ĝC/嫃5?Ѧx}^)Sʿ(@ P(@ P(@ P @Җ,Y|ē-[mƵsڨUT]DFG<$0_TxxQDI4Eѧa{.i w/Z]t?q}۴%꒲d./-m [U`$PdzNH P(@ P(@ P(@ D>Dg슖/_رcv[ƫW fkC~ϟ9'Oĉ]l(P?ٱ}2PLu"z|Ȕ8֎xmBDZN}?ġǦ1Sc"qNoGt{SE6kdqykf f!^rH5cQ(@ P(@ P(@ PQT(QEdJ1aAwm7>}$E0I Ql4m6,' @1cV"|>B-m^PZ6m:p@_E}SrKۧBs,BI)@ P(@ P(@ P(@%NJʃaC[jN]{boJ1бc-*I&KL"/^ɫ:u+W+Fƌ-\(ۿy㏇ϭ}pa8(-={}fiJ(]*/ҤI*KncFbΝ\}v>ft[$KyGhh߮{)=z :F+&>|dy nANa&Ҹm쉛/n^T'UMα ?> :qШ,UH#ċE4}SYso?˨M"R+-e&-T fsxOn㜒F}-]8: SfH:$J '8=靯\ߍ}+{ z{d#aQީx~s4,GQmXw4W9PQ(@ P(@ P(@ P J 0% ~$" JI!Rӧ 'U|TeVn­Xrzc o @Q&?>} fYMP_Cm8ԹW%uP$4΀by@QK}zec8fzi$c~h4\+> 8n$5b*87^= Hɐu3WGƗqm=8&̮:7e'b&N/ǹ{{x(@ P(@ P(@ P@`J|˗/Wb˕n^ 5ǑsmS2eukk>0l(Nr}K%;ڽ[=EaFuarhۦ,"Ed2j(mFY} *?E`RP}wyZ(b?Oݻ>r|/ܾmZߞDRQ̐ԣoٚCR}d,^_6stv;W %#S&rzv<;412=h&^f[^9ktSC(Nqa|J@#]Ո&{e{[KbxtY6&/?yv(@ P(@ P(@ PP3];jYGDBEϖOԘ;V5{o\8,wkF"qbW9d%ft?QS=ze?R^FB#Pf\͓9sM£GNճD|en_L3+!6?@ li"oe̶xmy7e"HM6ꅣSL|^$ofml)Hhe^v4Nmc8:CWҸʅՆ1v9c(@ P(@ P(@ P(=kDĈ{v F8i%v<*zm[ >x͚&!fLgY/?D-ǔɝ7o&e=XHv-OgfNw( 4!0qY-[闡)'^݂iuDK~S_u/WO^^X1og)|!މK[g]m~0Ҷ\&}xv gVy }?[Zx~о3s,Qw{zFu(@ P(@ P(@ P @>"P>bMXPѳ%e(T(5h1 iԨ~ KBb5!xylvUq \f6ֿx6N༠(@ P(@ P(@ PP࣋N1$ ^J[P.TmjeػOlKZʉ1շѸqjYUGhv;95%A-g(<:}U;}8^p!e6xR7 wςƩxxaQQK+̨ޖ gs"\ϖfmE6UEvśK;ڄfB1vB8%Ge^s<; 8(@ P(@ P(@ P(^DRA$MP:tC-=[P֬$I7nxs!Z֭ 73 >u!]Ǐ_OU*.I͇˯1ط@~?wlFBr 0) b`顲k迫fEJ7kwRC|`631c"ːxWlh׶xt$d{  [BҊ_pmt.W(@ P(@ P(@ P2cFwdϖ^@&#CEϖ#G.y uAICLsz3,;ɐz-U32$s[޺%QDbSG4\[W˖ @(Q4K|gO4ZV 4dor}V%x^xNqngVgl>rAr}|!o,{{pD'skz9||mhG P(@ P(@ P(@ D#D@i۶-^xa] 0y1sQL6ͨMjh԰Vܸ(<׮=پm4t޽J";ei`vÄ +-Yh$HOօUʞ#F |+0[vP?z Cv O,ԩ [-X+W6m]/cƗ?on~hR[+T]xZ{9wE<`Xk7񓦓s_9wo1[-)Nն<Ng XeEds!?"Hrb+(,,qϜ(@ P(@ P(@ P^(E ,P#u :/^i'OQ˗/cFe" ?'j88#6z횑HU{Μnu*UC^dڨSw0^RŬ=5!`]XhY\2`ZyX賛Ԩ~~_+U*}~ThԩT\rOk/Nĉ%.8w=zI[ѳXOWRVrx*gYe/RYʷZ .GlVۄFܵz!ErTCY`|uXMx׎Q CdW'gܮSFs/r{αo])@ P(@ P(@ P(@"($$eWo?$(mPFq֮ sn֮ˀMб<6!3g@ld55|.vګiР,޿ +wUZ8 ԂbDYXxmW׸b lr W ײ6ܺmMMc(WUQ eA ,=T?${Pe-u)?Ğ %0@2YbJ}RJhru.jν[qy\m^ Y+,1\q-?S:m"V*C{aLkê(a%q)@ P(@ P(@ P(_-Pa[nڵkx捼'O௿zg336zKLm|o8wL= +)d}Q|]vNgY}>5+i{xߒ}D0Kܞȟ?3dFYE=J̍Zr]+n gNԨbzaX eJֲg ?~ɒ%D:eAp(Y/jU`jwX5PNb(YTg O?'}` @qtvA>Q/:]OH!b4oXITlkhyr@!fpKE '887^=tޯ-njS(?TC°"uYͰ ewӰ0CS(@ P(@ P(@ PQ@(Q!.˖-S$bVk???4kb(?a[/e֎3ftGl-c)SڨcIY*,vG"ˤMlsML{ gDL*-mD-X4xDt[Y=}Ɍ,"8LhܸZ*ĚyX#!2lyc6S{PĨ9kvG\eNE$LCևe)h8{2X]R8"m5*iΞ@IDATr{M4K\=OvËËڄWA"ArG55(@ P(@ P(@ P"P" ji2 PbE%@!Q۷oѼys'L_x_pÆ 3* B: g*9@Zr Æ/‡M-^o_+}'RxyYdΜcFEDfu@ʕXdQQ; mBKClS3lBT򝖝dY&kPP>~*?1CD-UZk= غϓ|cPΞGJIbSoA*!9Ō1#?CͭCWt#xl$ҐL+ ɳ0j^ojgY~lԮTTKKc}'_Hɰӹ 1)Dj]Ɓg_BXw|'hޓ{4(={͔,EP<2ҥ;8r12rx*\P? I-p"pn)3#~ ->׏+ b̉t B|m 明HVe,~u^_i$\]3ӟڜ"|{gMLJ7:P(@ P(@ P(@ Pw54 X @ tTYPk)+P^{Zؔ%IUW^IeՃ o/3<ODƔAI7dLZoc6mwcY9 (@ P(@ P(@ P(@/yX ,P[H A|{)N"PD5Y#*6]Ƈ緃l1E,eOwM's(@ P(@ P(@ P$ @$ˠ=abϚ؇(@ P(@ P(@ P{ @=sq4`J4z P(@ P(@ P(@ PQX(QqRt-!8W(@ P(@ P(@ P( @ wrNH P(@ P(@ P(@ PPP(@ P(@ P(@ P(@p`JsB P(@ P(@ P(@ P('(@ P(@ P(@ P(@ PR(@ P(@ P(@ P(@%}s}e!֡kOuRU9t dɒAR2>-{4|ҭϨ?p0TΜ 9r-# t2κ Q: ^'lȪ!FX@@@@@@@> 2gHá:yjӸN7 7!YCJ<* 4-L-nR7]s?߾._ӹZgjܼn36hy#_]"`IF8t pU& .V<4-:N;B2Gc@]vY-(0V{iTy?ͭf2Ҹѭ4W~fx\"}ҷOSI2y3ҦrI՛<{y&ɪU=|1ɓ՜嗿k<{o*9$k`6iӀn'2{N)]"MG͋ (Mwm#      @ ?+({ZJ,?G{d{ծ2/X_o'2Cvy{?]>Z[.(IKd?5        )G#8ij\gLҥt/9@Z%w>:9vnwB? F6 3ߝ8پ}M)J 3ΝYѾL|9eV4i( @񵼎Nsٰ!za7KkZ1Z:N%o Y2a3ug/Fur?KW¥pmn       p @JćST]yRA+L~|N $E(UJTOjmڷQ-^ t]reeFOa;*(}45iSH֬nOX,3f5Z?Z:(ɶ|]P$v/F=eוȆl.T)δhg?N"      %@%?o(NBefZ޸:)!M˺@)WմRtݢ@iR#ys没gCӴu q@I2k 0%eή#JSB? v(5b\tU^{}ͨ|;ӏ'OwʰH89v4hfcϝ̜ 2嘳9+++ |LFό|(ONG@@@@@@gGG% J$# FP-qһN N@(zn`A>ezݕ%]UPo$^dr-7FPҦH+fWjjܼkVaJvr!}27٠pޮ_Z]"ޫ3 x+@ѡC-)rq3vҰ0L"LIӘ>NPrd ZȪgIuIHV9ٿuZx XMl?)SKvVsi5Ytl޼aIzy TYLTz2H\2 wEu/U27]{r @@@@@@gOé_,Xn\qlسA2 KTi3m+7ktrȹdŲ^ 9%THqP˺]dQzD7>]M*`OnY*9p$O\re%r @øce͎5vN\PҦM)sfD᠛7o+kʱcg}L)[=)odBҽ[CXۗX'7r#j,ɒnXTEmyt1RzJȦM$StRl٧$((th_;|PS^)W>w@,]>Q'Rs|7/LY0!-/lٲO,ɾNBWjd[w2$Ib1MnWE߿.T{Y!רPtlzHݷ_9͡Cǥ^O}Y'}C?"3S[[>^WM l. >> ?o;m       p @4qRiz;J&-<<\& e\s"~~YUXe߸N||dTc}, iTzeErY_]wǛץ>܍L7N:'&.%K~Ƀ[S=+פ[ R| )bQ>oj9rG߯~"z}K9Ѯ]¢۰seŸ;o4oVI4i F+3MΝl˗ZZWTJAcU支.νBainI>] E;i D<+<"%t._ ɍ+O9ON)S5':;6v@@@@@@@(3@%$H lwk!32;MIDcr9r!w.'sy0ӃrU =*OOߑILpF9Zf J.qSH˗C~$gx'64ԢM4lk׿;ڵQ.cƻ޹|y_۷҄i٥RfgEiJlejw'kqtB@@@@@@CJ<~PG&?* I'pnJJ[#tW>'_7G=      wJʝy "S @ v]Jz@ 6M'f3S}BM<3     "@%V$PbSoHH|fqKgc>Y4GΜ[yBe&Ƀۣ'('ΝA~KNyԆ1Ge҂S3>+ʕ3WZlw᪱sD E6-((Aލ[ؙY@@@@@%(y'!s5zJf^szʾcb>Y4F6-L-sDe|q#0bx[ɚ5~C7oޔ3g.ȁs!ٴilٲnwrH*7 )NqƢ!v%9rݻWv!6l NhBJ.mn~#F8o5﷬IZd5yA@@@@@KJ<~Pbp"R<4-Ծi \~>?8q>Ҫe54l~{ }._/')Rf#Qr乀3?- 0o/Ov* 6c'O,-2E%K&ӧO7._,uԉt \{)>h@ .H߾}e֭݉{=Еk Mгvȉaw9@@@@@ݝAPbnջKMڲJ09oYn^iz=n]K%$ҵ7eȯ\_ZۇS&?0OtG_ZN"GcӦMG}gE tzԪU?Ϸs/?Wg//zj:wXgر>->)T(gv7@@@@@LJ<~Pbp"20I,cɵk1H(*XfX!)S&G-*<#/tWG_VYΝx?zfH72Ak(o?Z@u/n  l߾]:u$I$zH|Iy7<)}~=Η;Hz |-\ ~5?      rlo{f(1'ʸ_H„ Mb~H((GZp^߯$N 믛K/]g痴I zPnqA[=o{ 8_e.]:sٳҰaCg vf~IwEQE@@@@@;o+@ LWbR+#8% ʎ;˟f3u^szʾc|NHbH"5}VI4\q]=&) ~/7]kX+pHmO{bsk[)mfI(\~E:,+6;5\JHP 9w _<<\ڶmZ(xpr!gLk*CܸqL/!SN3gn}.2d ݻwM?<ё#G̾ǪUdܹUJR+}ۜ;ZO>)Rӳf͒%5!Cg/_>I:y{u\V\)_}UۦMߍM^zɵkפiӦdRG3FN8aN788w"܃,vm|r1\7-[ >'N,ׯ_'OʺudҤI+WҥKsӧO7uԑ47n]cD@>Ŋ3Qt_ѣGˊ+^?o赪W5pŢ6wlٲIǎSNInݼc5Whc<=zT ٶ-L$X]q۟Iއ|^     m(w[<#J*RW}vp\~USv Wh#+o =*5>[L>߈_0^PZZ4$o[khF ]ĺ tQg5`߭}ER7|W`OȑIR2dH:q9izKnx,qc~L6{VO vFH.MWW)zM6MQ OZi Ǝ[$fZ3jʕkᘿcy-mͷN>v]^&8ԧwS)^lZ)'\>F|>jK~_ZjVH/=[f5'΄35wXM~_5T3;䷓WC5^աV}Pusx뭷& G>mB+V,YeP.\(SLWg@CzWxq B_[5oiҧOn]vvڙ`G?_eРAvSd\zf^;zgVϿ\Kʕ\#U*w8:| >(BxK|O?gtI9*ɜjc"T@V.6ڰ'tqUs.s?mI&hujPt3HTLka3*YJIL8RWS&h咣GOmΐ#T@qsfr}1fB&:>St5~&g\"~aٳG#yk׮):^ɑ#GD+`}-gEd4g^4sLSt(z>+,] -UAUEd`[hJ;ٕP>m0Vºg}uPZ=E$MԮmрsjE?sE5C;&͛7wNc3IJ,)uֵ8ZG@ݚ9sΝviK?gέUV/ڧJ봒i F+-[$jv9_- @@@@@"@0"P| i8TR'wN7kp9$k|XG_I]ꦿko}tvtiݦ&+74~k~YFy`I"9wAL\xH iZu(wrd+b| Z8# ^ilޏnVnfkD:GǸee_"Ucԥ2yR]wڴ~ӵ\F^8[YzG2MfsI|mk u [KEhud#J _iD-MbZ}jk˓'|1u(Z}[2ID?}nk)>k ҃mK)] Kڽ{twEmkif*~}UD^\KZlwjX)u3~kc=fM&5^PVu Z1AewM~L gz/@7_aW]2'4MZD+~ nݟVePC|ܼ(;:j(u~^4H 39:_z,ߣU;4Pp^ |;ؠKHiCMt6l#U?+r3zZƹL͚5=M{6K=9;:huk*0ƱP4A(U3ECɻ.AU`AsZׯwdUHEZ}ȑ#'gv=]f:s#Uɥ?>     q @%УzI(."-+2 ۻcVpnU&UOyPn1 c`pOǎuK0¼]_3:(J+}hPF3͜ıڝ?L YF~yBL9lҾ3Pp/PK ={?q "8+exiPAѐ._cUR4|_[+~\{Po֬Yzx4p6gPAhMpJ}c\X~78pgɃZw3 :y@y7MXCu~X_@рtH%L_K i䡇-ϣǏoM0A/^l c₮*l LW#^ ԝ6@@@@@@VRp9ӰSûWidp!iytwlhm;K$LsQt(TP d/`Zn,\״>ezXi;~|4V9@Y2׹滶J&I ǥnOo_CWډ.mRH2\_}s3Ԯs O?yǵT_:}5{IԩK:h?]6E}Y_''{x/LGWS6Lk9LO6Cef@ٺuVkIjFГk'^}T,:9KD7btM_mF6uZwF5ѫ sIY#:֘|r! )y]͌f8( Z7.>@8ʕ̱m~i񞻺h[q҈.3 fL*YCLS.ɓnuꔕPYZ^F +:t\ J֬2mjs>4Ԫ}ҋ3\""VyrR^y3~ƝҮH;G=,rS>%=?]{^*THus|}a|QُIP׼NSm/}]'9(B/:9w4h8(G+V<]yN:~蜙2+ i% b\K,)mڴ19z6[EX3⽼ǹ|͛ϴG5/oy/&Dۢ@ }Lk]J…u~V{EgvS}(q;<"(lrTXdChG@@@@@ @1vr}a>i@WdL8e|ZB>άߵN.^} ny#/o ʉWq]~5ǫ@ T%c:Ie1cZsGv~\߭j(9IIJhaɞ=e΃zZ]s/P3fܟE~@ivOF52wN/I>?Wf?i$ YeIK/_%#G7M<<Ϩۉtt9]G?e˵j^aZa劢@Ay_IOY3,1m4/͹ɕ+Wvڷu1 *x_Pƍs}M^;ēg%P@ p-|U.4}mÇlٲ@!ԯ_-se>Cٽ{@7oGSlPEt&MX{xLu["f׏=Δ r//Sd!ޭ=|ed˖U%MS$C4rYyz7rI/Ʌ %ed&U~lϧ;@9|]P$NRUeJt6]Bb J xgEfu@\êdmV'P_sdɒEtIDe˖رcO>Xz4lPt@j]~w߿ǹ> 2?U)eU<{iaĈ5;/⬘<ݏsu}ѥtpԫ[mN~܍ʧ~[^>&,,LԩQź}3q܏IE2Uy?Mڿ;͛7cǎ6'N4iҘӿ 4ȻKW|WrgOj|LF<y     @l @mXJD̠$A2HIнtV@J(֖8ab]NwE$S7} wUA nv:% kaY}F76EZT3RUd>NB^)Z^]/g{K))ѪPрT^5חtPKʾ\tCȤ$8e6 /ՑInA cݩRinsN B.3uJgɔwF'[wWPKdf~Ǻu䣎a;@Ay{KC}/Wbݗkڵ]>n'N/Dv^{Mׯoߢ~֎D5k&Z%6sLRKS~dڵ6_[PeFtӹKsǞ={DC?@9x?-ӰCŊMPGcǎsNLM51{WZ;:vb\w.?EQKn%o:Zd1:NPI׮]mE+bLd*Wl%߹SN}M]T '/}JXx )痧BZZ' _jRX{GwVɁ$y+S.)^Uƭ+kv;wK%At@ѠnW;^>i8zDG/ҥ+2O{̲0 >( '5lܸSڵwEGO\vcZ5_dbܺ9seEčV/w-"_;|zui6>wViִ˗3Bb9 'M$C[K8;ݐmˤK\n-s}'r65yAKhKHs毖#E'f/ C|vv,MzZ %4"S1n֬2tH+ײA9GݟNعs]_'ZIMauyQWs6_nv!)SK&VQC6>=Sl:EW{8/]曮e2ED@;wǘm۶G|]NӠ. /x܋u=`_kP -88|UA1ZѡgϞ0((HOn_Oe{mN ie |}h_s3e!9s&etY"S?Pvw.AkgVgl2Q}ޓhߏY~σGjw2k,تV*j2/eY4XN<)ʈz;D)WsH/_n>G:ҠAI-*H/=N&f }4M':;|u     q&@%#0ȍI4\An4$oF]fGɓ!I(;{L9"GN7䎜ewd#fzP^*gCr;r;P.s7KY~ZBOŷ[~@@@@@(C@%?n 9j{[l)J2&Mwg0(      B%>ý!&M@[C@@@@@@@[MvĿg!       @D(M(Qp#        @J@@@@@@@@" =@@@@@@@@@ C        @P"7        @(phB@@@@@@@\JF@@@@@@@@ @%N|j #RKv_n{A@@@@@@@X=s/)>oͰrf 9r_ll'(QL:]&L$?[% l        @@(yOc_Hʐno%tYht[y1LKZ3{u @@@@@@@(@% OiL0$LB,韬!Jmܶ\7v2(cޘ`>K>\~dChG@@@@@@@ @K~$NlnrנvXn3 K1=~"tF@@@@@@@,(=`']\>1Z _p~SvnOg@@@@@@@@ bIc9jTJ>0\ڻ&`B婒6YZ3zrh@@@@@@@@=Q}*X}L(u דʏT5sk|{TF@@@@@@@ q<@a^.&xwP@@@@@@@_ܣO@98\ܵ:$Yd2MwՓ7oHYU%fxa       Pk=%]Cf蝌}chƶ[Zy       {?D2ioʑdߘ*1z'ޫzh!       +@}S~ wpawrqr~vU¯_r:_"Of1arw       q%@%cɲ:H% 5ٍC-n#66M[ۗ@@@@@@@@_*ȹISHsM$c$ipn>30I>gs=o~~,ٹc@@@@@@@@D&O$J"AdP9z\9E¯^vȍg"ezˣ3O~;Kc       885tOT^{p) ;,`X%{f\:D{        -@}Y^.韬iԯ~' $YIغsk=@@@@@@@@ r>GJe;߲<$w<2P3PiI`        @ 8(&דK{DWQkjƭؽLF9       @G?@[v3]([sjp        PR62riњ-}EfK'䝅 5        %@Œ^4_(AY ?\>9Z#դvc<&(~?])(HQQ" 67;"RT FQ&]QAi4.Mo,7ݷ/{x.?^d2O&f[O3⇡2sgb               0(@1$4E\gN7'YNh.H+SsJu_ڍIHHHHHHHHHHHHHHH(XʌI ˕WgYt xfҿLItҏW|$mnLC$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@(@ĒVTgHPQ7;W\v ~ri>?ArHHHHHHHHHHHHHHH!@J4.bLH_'ɹMr|\¬*(THf}|WT31 P&e                P.&&               pM$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@Q%*\LL$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$&@IHHHHHHHHHHHHHHH"@JTHHHHHHHHHHHHHHHHM7. DEp11 (n"\&               (Qbb                7 PDL$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$ P$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@npHHHHHHHHHHHHHHHH *DIHHHHHHHHHHHHHHH(@q2 @T(@  P&e                P.&&               pM$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@Q%*\LL$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$&@IHHHHHHHHHHHHHHH"@JTHHHHHHHHHHHHHHHHM7. DEp11 (n"\&               (Qbb                7 PDL$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$ P$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@npHHHHHHHHHHHHHHHH *DIHHHHHHHHHHHHHHH(@q2 @T(@  P&e                P.&&               pM$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@Q%*\LL$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$&@IHHHHHHHHHHHHHHH"@JTHHHHHHHHHHHHHHHHM7. DEp11 (n"\&               (Qbb                7 PDL$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$ P$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@npHHHHHHHHHHHHHHHH *DIHHHHHHHHHHHHHHH(@q2 @T(@  P&e                P.&&               pM$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@Q%*\LL$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$&@IHHHHHHHHHHHHHHH"@JTHHHHHHHHHHHHHHHHM7. DEp11 (n"\&               (Qbb                7 PDL$@8vY sgޔrsHHHHHHHHHHb!pw{='_mYK6܇HH?BHEuהݛo}"?s[r\٦u5ɓ'.zW޽x|R|a}3^: #";os6Β+G @2g,-[ԻoذAƎkV ݑSʞEe沵lצRߡd_ ((S^%N)C/u@4^]峕@x$@B>_\'l/]r59ے̅|&sey.wAMF3 ^$@̜Qw?//L`sNs4+UY~)r)g9OQj$!p J:n?pD:dl2v,\NXvM4)YPQs!FiH@:|LeGsEk~s?nQFO[w[p% |Mj$M&h T}hU}ju߯h3eK@r6 V|R]دW%icF&EPg,J(&P*Rhxϳz[ڔ)dpzFFVP$#z)k'؟-  BBZ$W"M7e+\_X '!dvJj,Vd挷%u)y@7,'4?:GˤI_&l >/tg%#G}&-H0mRH*U 5m]S|N1MCzh.]Z4i]vәsa#Gihذ;F!O2R]Z*T뇭sɨQd֬3Uv -Hƴ7`:7$D8IbwH8a6lu@^Yw}~뛪#=:ҤL'@J :Nj >>_CRR/eE&dSdp7m-]D(:rs7?ż6.ZMXL_&/~-yr㸅n~KcT4,rJa -@Ə xASjg#H$@$pI咪d}An5>]R@t&*FsT!x{?ns̶]'9rdaCE`SL#ۧp-;d謈"nr_~Y=V[o}R[:uJ9#9 P\ǘV>;S`󳟕]eV\9]t1O>DRJϽqr$Kk(^Y=;vΝ;B̓':]v?!sIy!(#U'•3wj|57QN>R;X;ogWSNKX\4c&lmLhwsVy㞧>z˴Y?n%Kk⾑,i,Ҹ<{g fi$  PB#m3³9prDRp^vw?ɔoh??!׈65uV'w&:?ż6O>(61}Ŭt2 $5|G9tD׈7DRvf,΃G&)$OK{ja\?=lHHIfv[ۉxdPʶ{# qW]:i۶gh"i +@q{\IrR|asS$eK%mP~>ս'H˙DuJB@rW[rU+0:1[lr]wIڵ%M4~wҷo_g3I\̳u_YkU #Պ;r!wDJ:>¿ z`>m`0C4d@Jh6na[ &w}tnӧC[t]Jrx^Zz {w.@D.%SkepaNfET&" oU߾;~BVX08dtcٲuy/JVEsjT4ҽpY58(5y*#H˔(J!BJ0*T> . $z %@1weYE=PҝRt&)R+(q$)@c%4K>NǖfvwV!P}qAJs`[ &BJ0 Pyp)vB)p.Xye፱{ \(@I&:*rQyg{T>C KixUH˖\7JʔWə3ʎN/_癿X(W(;֒ʥ XϳrDOס#XRr-YU*څd2k;wi<̆r*n$W*W'_};}<]z&`;~Ay/7Mcd~ٕ@)r5ix~Ț5[–ȴ=u\jw e\%KܿU*۝mYdӣGO80Mޖ!@IDATYoq{@qxA7ҰAɒ%u@}wl޼s><Y޳fVƹm+OW-WtMv݀-ZTW.]:{߿ff,?YLSiѢ"2d)RkoɨQ>=zf 7Xm;w~Y/͛7tMoku*٭zq2tP)LQ` ,PmlIHnj\^{5;gmM6zmm5jvsn'OV9b_~9q2lX`6wr[oxvy饗رc,>|NϞ3gzWoOJn~3W-UyۓPJ;e4=drDm*>*&2Ee\loXΪw۶8"C~KuՕRr%N+ɒ[ͧT7* SrdJF!1qٶG Vyx/=]\]YD؛lצzO< oN{/lY_ygѻnn&(Om[1w>&*}\Yr{M1$E/q訳̌XL6?KԴ<]dݎ}!$:?s=[r&\ _xe*)x9 }->6y8O.}n䧭~>w |31c5 ]Qq t'_PzթRe\ g<'OT6Bd=z28j/ ` U{@[nVK/(Xr5i-2d~vE%ٯ Խ,౓MBHToÙ_5 m+?ʓS'r\;%7^!0ך%>l,<ثwV[OJ9ǟV8~WPnMrckt;z\V(NNPpju@?R)Px"Inj_mgIlq}#s:3SȒu[ezzû#w"ݞ]Ny۞2F}:X< ܖ]gmX{9_a/S\;MRm/zܢUa3l=\ۭ6.1WJlW1} oܳ`;/~ 0nQx\G.UR7V=оyXoqnܯ)Pqd7 ۜl.Sm w"g,iUp=yJv+6y{]%0'DYo;8mPߝFP7 US^M}ӂ=p9+~1 ;M%5}TRXS>H2~gzƻ7lcAlΪlK/|x:~نc<2,ʃ^Uqf/EjOWon_,Eɓ&iSc^%ͯeKl/e uR]7JNz~>;ʐ]ļ_(J| @ʺ#Ly   PEłtIϪR{z^*|NAEj..v:hÝ^?4]&Oʽs9C\7ȻRTq9$ϵz7uk(%%k啮CxՊkHR?81i]HJFev"yI^c=ZK /xZ5;\c-P֩SB6-27%y:NY<;IYtZdD*@=]aoYv߹*9X .^~VZQ:t2ISꓜWssf5j$e^۽A;s .ٛo)ۼqFg]NtClaC۷W"BzW >,M6 3H% Qь3z>j2L2s@2i+EX޺uVRUgϞAH-+/_>+ \̚5K bb7Aԯ_?{3`^vWÝMrgٞt:~i{Hm۶}'0^b͚5 դh~ P=_j+H>Mv~+;C@Ny|8|̴l[Kׅ?>>d$wjlx%Z: Stx>.c$;l˾CҨ9^)!*jzN\ Zޜ7Jjż뤫:)V19e;OӈY*32ϏpiR᡻=B_B~wUR^}GHybAgw aO7`gF#@kGmoa<ܯ>_w^nct*yWu#'OCjWECǝΩA6n8p^{F&XR;ZZPйj u?~Ԯ?nF5 $u}^%U/ DBt Є+hl_Hfw`6 hk/[s6ATвlAn(C{}{~$ߦD 茄A`'`ܝv:d!` gg,W4.xJԜ0M+?{ K^fA2Kuַ>/JoFd~U>V6϶k*u=9oy:ի=&y(vj%ə9g^9y*@BUBZnCFێp W4J@Uū𨟗Gω'P7 |̺~W?Ք7֩^ߗzٮCKO*Ro g,asľ5[ nUJF*<%(Z@:?N:y#ٍiHHC(@I&Ŝ2>ر_wT?:E ˏ?ъb 8>R/kޒHoaAy^SQ{ϦFOI}βYIiҸY7K.\5aթ*5bo P:u-ş,G[ꎛ9nPR8zF/s[H2y2b\g?n`P )xG \ dȐzJ)4D[0xK/K?PyעŔIfe`;K^{Mjۥ͜t :ӦM$W(Eѝ5@b BuP0ϩS x@[p PLMV5{7dʕȕzEfܹs;[޽{s,ׯ-@1ۧ<\:b "*H(ŋw< U,}"O.a[ݻh*N$W_}ULH!UY >С *3cƸ:k:)Zb;sX l#c6PLT6U#H^l>KmmRP \S ]~ЅX 玑\<`iF#W=(Q ( %j:W$6n<(9L̶ԫ|50NH2ꑚfP dCNcV^=*q^)q^@l<ެT^݆:mmP*GykoYv}^:c o(aM[%²=%2\hr\WەobՏzu -@1YlPFDtJ7Rno?}Uި0FeowD;9׭ن(}8%w/+|Բhw{TʬRHZ+Fxс/teQ^0 ݴU335(<^ܘ-󺾰yCxoo/ANb(_ߪOᙞ[ݿ1 yTPt"kuJ V+j3\gu$ɘ&sFdf"}e.(^^ &m#CUl7[)?_m$ ߣNŧ:2c-QU-.z䕗봧NVDRGXqJBg[Uv8Y\m4m԰TrHʏ<!ؗ-|HۇVY)=~v"Iiذ^o]t7'yx  #0A:^a1ٲ"DGjz2XBt|| *"#(ˆ7uc34({B>wYY6E+@xBS7Xx¨&G:\0\_/*12NXDg~k꽽:G[ )̛zV;}NR! Po^xXQvcw Pѳ>xf2Bp?kYs-@>Ձ O?c \5qeWO AaٖԦ~?_cmxfCk%t0AZfx maGzM^howd^ms>GܧrZ}{2kK %^XڙoQE,3bgXgԏpaewA.^(G :t0!CȆލ*dxF4`3=}vx2^¢$z 4MM]<*QB$} yZGҢ q]@d,RIכJX` ;m)o^G\m[׬T!u F(o(y͵z% vYy&!˘V-@;xӀp pSh/0/oz].{ԙ>B*5/˄x^ ܡp?ܭB~3zX,V(7mw?ZOYܷ=^x {?Gܽ[PShnXa 7;Ǐa |5wcxK|mV@ze?Sjp֘PE$ (ɤ?E>tqٻCԩ p(({!Z-XG鮄nxpG]j`=I\8dtNקxdOɒJ2~V9(\6",G"@Ap 7(}^ufڼy\xqjx送dW%J<(/v p=~!  u>D5j#|:%VrTd0par"EܫcQZ ^qyYE$ԵqY[y1CR@'[u 7(vH!-:CFNIY,BD'<^ y血 &mIP)a,\XeZ8йBiݺu1˕+KlT3+Jy@`LDŽ[ls P Y,Ӕk"R  4bY63nbh(5n8ѲeKM>+P͚5.D>!7Ǻckqӌmt(X>R8mK<$ކYf,~J/R]^Vp#pAPбLu4x^v LUt\Uh({܇* :A'/: 4jvP*ff¸n/ a-ZJjO:nٱOw:VMqίzUbw :MT(pmzgF3ӈ~ v@;2G{^My39!faScuXEG KvgihbiU߷IvQFh^ ,t60#R/QAbyF# ; Ek bl 7ç`z>~܇)a4Vu#DЙ,' @pηfShDkއ!0γ"U[<xK04΃;~%+6um+h [J[Ny< ,vr P OSϵZkX(~WC'pef 0.*:2 [lu.^݇j~ԫW٣]g8a?/X w6QR^RFr t$fK$@$ PLjO9Tr=?If 8 >..<^ם&vlRL{sDa1Ty0ֹsyXW5J`v;~ܫ(P@o1A"[2ﳾNX6m?Pޣ1/%ҧ/)] :w>=`esNQ#_v̚M`_,P͢fYYz4[n6yUjgd >3_l#'35ֽu]t7̔a>|rCBܩB~`a-F+Aj9ƴP"zƨ;X4Q1أ0J LCY,FҴd!y@jI231z6,:V&]wVb @q k cp+LW*@;H<-Z%a-wݬC)1^voyj~kGrюNk8“r< "/PM'r,wJpsax`e4^FZN<\R*ʰ^8NK},HL}?6bb/~7AgN m,%Un̫~ Fd Wܕּ͵awhۛW$nDH"SᱍO!Buۭ7*zKա.> PjeȣŁo::?]r&4o_Ct|v]L[Xj{4 swUvc(S)?PVg[b~{~S#%C-G>.# H(@I$~ P:&՟yU#smS-R>#J;E#@ɑ#_<dv2f$$@AF2&jo PR:uyw:<ӼgsȕdU-+KJOwsy\H _: Auzfۦwg^ZGNfjJ;Jt_ \>DD]2rHI6>wyG6.!(^!&\hAٝp6ëﴯԈ#} vawb獻ѐ6sѯz#^r؝EQ0sYۨCd;1:0a!<|x.N)/qbi+.ܞ*[.z#< FU!3 f,I랢ހG{^Piw'w0ء߼)>ly#tW>Bv_|z|IH.< P.DD]>WX ,dɒҬY3gu(O"N$4c PfΜ.&D!ƻK/7y[/y>\f͊te,=^믿ʋ/ho^qMEԩ/ 9G}$]w^ce˖-Zc>p,c Pr-o'lѢ߿dLmǥAζXf+\~pFf]?R%4tX(<t9 8[Уbn- f5Ry?:ڜ|7NG.ۼ彦$s3\:ܰ.\ p#K0tݹ%gF8OvƝ>oڽ_>re<;-c'Vʜ"OL 6٣y>ΟGu<- LCv̙tfjw^WSX nS}}K&-@>l i2Dy24@,e,3`d]dK\ݦO9!Y lk~SEE^SZg}ZyCh !bY+^}'1᪅Je%S\)4Em/'ŏ%xtE*@ Nnxz+h ‰s=,kR1eOw*58fOl P֪y==\&U^ð޶h(~}% ?l)Õ(>+t7O6 Ʒ5` .rF$kk[;1a?ګU?Ѭ(^$Yk;Ǐh7kLnSorM9yd,p  K(ɤblڴSyLgVF#@P~By﨎wyxؘ1 ͻeoAX҅/3f|+0Iǟ ˵^ PL~]u>_EW: HlWqvm >WcGQx PP z۱c-)Y" m>5k;l;Nٳaڴ&֋n7~a7> ]tht>DD]2e<љNXBcuA  vAg:B$u(7ۓ-3׶(¥^m I2D>-v!m7?W^)cZj_~7ٳg B(g;ٳG{. 04vwM7`{9r"9rٷorzY>}z#uœa8É>vȁuVZ:`yWj޴_L<A%ڎZsGq0'DQP!;F'O{-JtowUT`BaǞF! C{y뇝<ˌ0*W`i:"~6ebPܝpiBy(#j$1“)N)U,ӈY=G{^ubc PPlmGss_؞} Vf:i\AС CH /3CݐO%! J,{s(Ӈ=40ѫa!7n}Gqf YW3ƬzWR-`؀F%.qgzO<ܴtڣgHEf?PBǕ*6y^l}"$> ʓyfa/;ɯN4Dw> ,CePyEzmҸ;!* b PBJRнfy8b~_wo%a?ګjMk ,mPxF.]q2NUUp9C$@$@ @J2"!cW$2bvҨa`d޽vfS|r5R)mٿ =ŇZx@1oHZ1i)=N}pxyo5*z^ zk6-@;w`;%Seℸk+ܑ$Լ-^m0o_CR I-"/ vG:.< mov_*ɻ|z#gy8?(Se={O1}擻ӽ{w"P'’?jrlz]^t>vϭzⵛ9%İ٠h({kdaX4Ϊ7^'oxց;sch<FKߞ^ J \0xi?lm5Ug0vŏz},߯xA23Q&ۋ;)KzNbು &=qbi+?...4SQ"C Bp߹GM <0Xɤ5S?B6;LT$hvv^P< U:f,9HE~>_cq=p{/{j1ɫ"$~e8bZ^V`& (١50JXm7伻7kYq;VkK ۫9TJ\ M`AʳHa$%gJb~ U,6gwJ5PvX: >X[uVO3-  PL*ӄ̓+eqև ҥ0C;;sΨ$@N\[tW9tf#n$~Ioagޞ֫P%J )ag˘1CdrK-bu6Tgg>?JÝmǃ;71}l]ÏLlǧ;?fP}"^`CT?ŵ!wP˶PmRy$\Ga>(F fdwRbj$٢:H<m=:8ux@?av/+3w*40$ XFb/FM{|#rј]OW׋!@c?So;Mt nWYo{@J?ګ Ӥqʌ*™-@A:l jum#=a o P.k,K* ꓈{u v t>v D:m6Hq~XN;hf}FH$+aἥ.Έ`Ig0 W' !@yf-[+c^c6Dr4׆ߚ77iKa-)Pn8/!MxPk+^2c7*}߇̦#Kc }؏wB^J58QBKVg:>G frz95}"߯~L+$5Y3|2  K(ɤ2m&*v'㼛J|P:vM8 |Y>NuFx3K֭[Cyh^}ܥҷo\(lC(+ԗO~\|R'wF}2f >fflO+X4lXF=_)CJ窘,%9 PSzQ_/Z-=zp;-+KJ'ڊOy 糹}^ce|o9̚n.|#QU]vi۶+^%H*̜s?IfΌp#@gP4wz s՛_,W{J˖-C oʗ/wߕo&h݅X@^`ԩS'QC o0"1_!H>NnKTXXG=}zqlcB\h(T~Z-[HզISժv*@ygD s/?#j=1RLJ(^mO,6l.]Y8KyHJ*]֭[g4ۻFD]vÁ{+t#(u?~]J ~J]rt\ ʻM3 5]v;6{1χb| adlܫ_Jqay+?o#oN #?~iIqU@}P㝰kE4;FWd}zFW~u^;MNJV/g* FD>A{Ba.wgٯz;@mÌG؊K2Yj#+=ugK䫟mWF #Ջ ?ګj{l1ClcA}yݗ+@iU#'ts'njKr<_cioIUzL rL~b]EzYFvXJhlAbHyWk{,E<݇dd۸:gCVx<Fd<~?$>6e ܪǸ~>g30ᇱ !@X ɗ+>DPjs PbmkG{^mY6*TV{(K<$kpo*}؜Ώ"d-۲d{ܷqHH' PIMS>Q6ǽ:4ӚUIR³J/˟a^}^(`+Ԫ=HL52lhg5JrSw_%%?yqf.24iRO6OF^-e $@N*T(,qcg?o6kT^)cu^)>vLW5<0"yٲuҹˠ}Q#_V!nի~Wy]{3߸q9UI8\3 P #o%Kڛ$b ʕ L)wavyohˣ]uU{VJϯGI5kV"IN8a6G4tZny9܆=2fPorқ/^,^A;)v믿S;mQNM@IDAT3gQF:hoE>}5)%H}rJbdΜYԩ 07G)XlٲMpMAbXڵKC_H D8[ngk&PgϮA ;TH/s_h'@eW_}z+_|EXjk^ek\'E nxYWI%G$vx|D\o<٣ѱSBbA1N0t ^l %$@?Si AǑG]'^<{PJ><4Qe;w\6~:h(10c^5GnvC}T{0G=57q]bԱ Mlҙ_z1(o7(/7]NXL_ƜBKA'1ŏGa`)sHqW\/V^Mi 35_=2MɭV4xA ͅ0:Pf='[ѻo^ pW-۴z}N3!JΧ_ɊM;0d8ZGyw9K{~#!B ׳׏prEp#<vAث蘄!NvL4"}|}|eGu?)/鈵6z)-BPj=z~ w.mC,ؖ%ֶfGsmu;ϋ*΃GMV*le55z}#)PP8?9nf=uɣr }ǝJk ~܇h~ԫaj?:L` ͼ"m%>p$dL r.skʙK{DF$@$ PLj "/b,_^ugUs)t &,SNkh:xwYr`Εѣyy@7i6śN]u~LWj֊c")7?k UEuf2t z?0yaTMXAQӧ*Tm8V%0A=KN׮ _qeAPRJ%O<񄝝̙Y7.h;D9BGM@$b[(Fvxiˁc@dv .sVtp6l̞="hbB$b!zH,ufcoRⅿBw{7~aoO`7UDKI<{ ZB<ެDFw`; ޞb@vՑdP?:ͼB Pڵ!eJ4h:ҧiljFRL:)x*?܍ ^֯_3ONMz]2D#@)^tz˯|,KF!BdG.DW$#ޚU>}vjFq|kH{./P:3]RfPuht2w0ҽ۲lٲPN%2OHO GVٜBlɒ%'^?(2V og}ҽ{w Bz& l0`@<󨋞={j{rBּ:$}s ??hXE&9|oжCGVs:mg=F ú]ǝs/(J7Ձb۸A^A v!(g}%$@A{5obݙgcCedX okB#k;O>f1;X(J$F!zuw%Tە_t~ڸ(UtBTn^[vf}!xpuoL^ڡ~܇Q-ԱhF~OWE,W,xT//מ'@yͣow;mhg7%1^]ye z55iOJ#p{P8<(Ft8n)i5eU:G|8o!U6?Ï)@Jrg{t`g9BK zރ9!Jm͏kN%P®` ےJl{59ta}ǵkֹCa]4 o2c@!@>2^[_Կ f6S܃ }؏aw7ټ0l}[b$@$@  PQ"t5qD&Ԩg֡C2e;*QHWvqi԰]|p .-ŵX]Jͱ]rǏٻy>l$2pFv%v6cj(}j7 e޽ŋԽ$rӘ$ϣ Fx6pԯL*W.?/KrM\+z&LH/?UAwKNYKט]z6UCTR!#u{ĆR˸:'ԩ+õeqVHTKu Ҳ=[;BW~n^ceҏqe**.]wZ^ܣfBY!ZDq}lvX2ftGj j#}W0GK?tX3SG~2\)#jEFQڴiԩSiƍjU bh+M~D -k˕+'"h %&45{AZjQҥ0պ{"MFSMw zmϟ?V\is6+&>Ԇw`cA(@p0#w>E#+ xj C%hƌv\twnS6m7^_ "޽+ ⎖X@4j߾Y68pp?L7ҁ%猁+ @jGs̱k߀Tjƍ!H kLZ!V#>h5#;7$+J{Ds* hǞwrnnc3o]Cb%\$o.=j!O^`Ui`،tn2xSPpLuوF]v!% `|ÛqCO؁5prd*Q`ĊmT jMJ̍VP RDpӪB!`c`yy_@.rTJJZQ0iRK>7HagtRFix_|Ǜ3WyU3]m`Ռ視A؈eg/=P6Hd keR*ˉø6)HߦMq<)wPȋK/] !,|Apujp=ىw;+D^} :XDG{3 o͝> B\P$`4rjX\g"םhY&w_ElAPmL\$\nQ\nJ9eI+榚P*5f,>AB63w܇AhT2%2|_ *H/ #tLU2qVOdA.]FP|+}'z~I1sAեqm}]3IXZw#Dyoe%HWȱki `θ2'ZYZH.ITmX0k~z"W;kP9P4~/?z ) O{ Z!P„.g4W|0ꀹcf^-\pwg&,"_噲1.3VS:m>|2xfS';?RП&Ȫ>Ӳ uwr.nҀ xܹ]Lzǵ; XX<8ך/|nO8GoDit0UdV۷>ۿ|pN=›x1/M 69? tK..>1N%"\_7>o =!$ J<^:6ӊ"Q!w[zW^;k`^W (b 山 goޣkZ*Aź!nz$\}MW[t7%I \τ{\A=FcPlXl܍O& 9)wdP=nMpB\5w݇AH*\q`qJ#p>zԴ`ob|2w܇TI@k>-N>іvZm~zy1iBJ%\?yZ W\]*b'pL|*+yz]a ̫ևU 0_ nSu{6R߇ҮVg;iy~]b-?F``ܛPOd#Ќs)PCy)5[(H6!߷o_'F5zF@UUAӧ#HNWDF38 #*ε* V70f Bz`B&ePB"*4bM|ruuЂaYlNPZO\nEN s1д^(!>"vJx ö+[h!#0@A (!d"v7Ѓ~0Jvxύɓi9\CҰ˖-堍.Y\L@q3.::Q ٽvi٫ɠ Vb)F x+5P%ou7-^vUtC]zo竊>X*w7k,e*Znٺ%ܧT)MvWv 7?"VkH!ŧ̉ݣs*0#@ ('dB( 5 Ըqѭ\rQ޽N0moꍉPT48 T fJ-]yn>8J)\;fKDֆgi޶C9F +5Pp ] ?pA(NLʛ!% n[\U'?_*\9#ت.^3-M.zȔ]|wK&\$i6x&:#߱1@!VkH!wErˌ#0P n (A 870Vtܙ'N,x/_8`'!-[6j>ɓ'Z ǥCBRu.k;Q6Nc@XY!hڠ0xfQ@k! + mG^-,0A@XY!h#t8`F `Jp!+WRMkq6lAcF+tԉ#3i$ڽ{wX`8$3S(V1Bx9bQ Z4?=cBae6!/͚N>~n:vvLMEI(a,Mw ޷9XzǍ2#`BPi(g&O5g|Γg4{A%TQF +uH'״<^N+߇0#5L@ jĹ=F`F`F1o0O!#{W#$^ʅl `qVF``C (=70#0#0#0#0#0#0#0@@ (acy#0#0#0#0#0#0#0#L@ 6aF`F`F`F`F`F`F`F`L@ ȣ`F`F`F`F`F`F`F`F `JA 3#0#0#0#0#0#0#0#6`JؘG#0#0#0#0#0#0#0#0P zn`F`F`F`F`F`F`F`P<(F`F`F`F`F`F`F`F6lsÌ#0#0#0#0#0#0#0# 6G0#0#0#0#0#0#0#0@!`fF`F`F`F`F`F`F`F l 1< F`F`F`$G1SD>J s>{0#0EDR^ë'D?9(I#0#0#0FbDFt/ғr'K?+Oyx#nF KvЋv634\L"iG>&"#}aߜߌ^]e'#0#`@D)u5Vo~XwR'G]};_NgF`F`F L 01"sTԡO2ghu 9RJL=/{wm>|aib9rB?G;wsطr PzMlg?͛4-DM1bDW|o6tu^m*Adfz]a@DM6rhϟ Cvh;A_(A6]uq\&b't1(}/cω9+)V!=2|˿>rtޜM.k_ÒN}M==oƔ:<爛lP m/ҩwYn# P6w&ʘ,ܭ՛Pk"#3)˦5N&O~ -hi޶CQf4mgA"4}|T zBR4p{?pF`F`F`L@ @tҹy-jrBfАe||^R]WzۧKZM8~еk*[&w4bM_TUc߾3ԫwА.@"Dhwi\ܲy >L%Kutx꼆:؊)idGj!=:7 CSF PE ߋ{=q]x9B/_ԒE}m>xZj퇵Tʳ bɇ͝SÅGUGPQ9PJ NS%x;=xM*4bNI ]$\]Փ|oo|l[+lWF(pƬکs)w{j2vjUB)tXjEg?Mv [ *Q<9ھ 6Х;B+J%2 w|H,'&]x?t\[ 0žϞ-2i=yZ?W rF$DjDh8px2_-=CjVF`F`FE0%MV`kJCgY#ҥsJT(Q[N) bS̘7QuzZM' [Q^O-3ۙa:y (H] *vU\rԴiSYfrp#$9g]iܸ1=D0/Chʕ+SÆ Ǐ4w\Zv|_# (EU%[ӗ|/qdDH(A-^lZu*Jr)N~JmУ]^AA@4Qf I@1qߎsSƷ4dׂP MjKvUBfu@5]4^9!u2MްwPTc{HIQO;+GO@S̔?N۪[ g`F`F`F !P6a.P3P@?kx(ݺajR|ވsM_͛Ϝ93 #`qH"n{Z kM ̖g 5vS.U/%sVwܩr9RDݡ;AYjܦ&ŊUb\^ 0ꖥ I-^sO\.بd>D%|8*Z?'6=e qǀkqhhVc@~p@eCZ2| J(U77ea1$-=oq (l#0#0#egWP0DP%9º7~ga7w=ӄJN^ޥ6kZP DQ:*۷ϖ7'ONYfuR1ٳFs t!5_Au&7&o̮Op&Ď&o2Y;xaaG8 (< D"74cYЫG 9s'׹ReÖm !5(㥢6F?GW'Uk\lە-4aX'Kr6F`F`F`B'L@ rb PZ.x[ 3&X gc|j1cF޽,RC2w q?}zl 8y&M۪)E2Nt%c"EO?Ō<ӴԩSɓi=uNV(]t7n\q"te;w<8p̫%IDXnݺEÇ[|#*-[Y޽{G/I|)}zˆݱchƌZR9BQ@?ӧOE RJ4iR!?Ǐ;7MeʔyVXA;w P%jGp$p2m4:{^U;wnS~HϞ=&SLԦMYݡChԼysy[ƳeIhP ֠AUs,oibMcmTvm0ؖ/_N7n޽{mΝ3gQzXLJ`ذaK^J%nݚҦMCZ~=mذLHNJ8(G KC|oo_zq ]߱> U߱ Eg8Rdztdb p(9FOONeJZ Enq{$Z>{aU>m"$e NLYHo)Rf,w5˓g3yWa]\]/g%g_ <" 'Wn[U=I0.5-cL:(~ #\v{$ bˬO C85Lk}N_}Nk j7yR93Rq(G4N=EokĪ8I >7KbD,_(~\*;?bLǮcS8KZ*{O˷bT6wf#i1z=se^yIJEo%"чH;,\zM7\n;~]ͩCbO|,~`E;?Z)M߸Onu,-mVq:S+$D%GDD{ybupup eaKv%#,)S|?RMZ1Pb(x|t>F}'F4jC^ʒS\_Q:ŵF\XB;O]&ԫ;UJ>pq*9oܣ5OE)Pq+]xrkY'#0#0#&,Fuu{}ia \ѓU˷]TA}PΜ9e4W ։v}ijvZI1ˋ8oTiܸqja 03+͚5KOJӧOs5+W.>XZdIcǎ1eC&8u {v*{-V dgkh`1~yB (`W?^s?[D-',ӫ \2ID, ]_ο"޹SX@O/1Ǒ#0#0#fDoutjO[-#xz7LؠL%hq(c-V6"εcuTIPvxNDoG\(ފ>:.]2@!ny# }y.(K,H% $CYuD@P uN>h"Cnj5T\"7v`AE@qǜh ub&.qP|yךhѢКV'/(^X+ԠshǎVJy ( {{ (WOǏX5!Z nNíӃO>J!ޤU?JYiPdnPMFl {xxȼUpr3Z0Kln߽{W AZVA;=?u@Aĉرcu" >.\gI<90C@@%h>5P QR6U Ri*  (PQh M,5:t(>|A@<켢2u zVr=Ks ,6lhPJ0Ƿk#Dqj,TRQVQ%xBԉ@a$`#GoZWDY8"ǏP(Կ\ abͪI׮](*PI/4TgpM Jݦ̑:Y 'W|E 1D#m"?T(CcV@lPzvktP/ /r0#dQ̥)P 3QlP,ņf׏j-^*;EBX_2BP5g (inSˢקV7w?]}#!cVԛņ}/Bzbl_[' \ݺXNVR0P*T*`PF@Ը dIRDwN)I -/6:V^;*ie:Ėc (Ug/'R$C҄:&_AjP ҏԇ|P>82BVY)3NA|]] B) 6@Y43#tQ(5҄9@5D;{(=\CȔJ?JL3n5J/Ͱ|S6hB[bP:@"&("j9H}3Q T[&ZDb>҈gXpݵҦ h_G ''@ .:K5wΫZo`(ǯޑJ. ik mChP  bZbv@=;֫;} <RZ%'@hj*Ew>Qu!}/LiAE@ Ń2j k0JU:}gnzq꿭E9#0#0#0&)&Ϊ7s 6v~jT09r ZI& ̝K$;w9Ёbʼ~w7׀MP천xW_śJ uꔤޖ7?qU&,⊇>~k!Q=JjŨMz]E?3S (HC*G6l,2k:EPP5'*a"0#@[6B~Z4"Yذ1<Yl}cۣGo[6X+98{꼪xb샇 3XӧwZ@LV_2J:;Gbp.#wӐsnR7n,B͢gϞ 1y;T04 6С;+WNĄ'Ea@IDAT[xQU_/ʍz(hdW!Ռ3)B# ibժU-0\d$<0o0 \@Ȁj̀УB57Hɟkʔ)i̘1zm۷o$ -ˋF.@DRM.[n:d 3ٳFW, 74kԨpQB;Տ*Eq H30/M|o_}?J*Ut +/3SkPӧaLjCơMܳT5u(h.4XpPRO߶(}6/IߛdSPXMuz;'@VS (%tm=[DAjX߼+úST jW (cG:$J,B4W@f|S2.;z b31ZTaD <g(Q⧡4m\S62W@2.0!d"lZk rs ?Vf/6t;N٢zEsQ乫:vW)0lGv盎n:63ݡLrP̅26- 4&6)TA>|V.\ |JMϯsM7$ݾh%h^W7D`ryP h]2 GvycBqT% GnMUwta]?a)eV ( SAD#Ѡe̔C/c~9Ϙm"T0SjRР%g2 N*m:zު(`<0vbt=rH?p=s3wk@ (/ ;%kBY{t! Mm^QҺ|aZa]hkB#aE3tDR) a#kDŖ:V[lp,*`0Wݯ_cF@y(  Zy]K3{… o̪G4fhh2ߝV #0#0#VJfMTj1[S_bi PWx#dSU3PA ֢Ee- ibL'u+WPVɲu W_Գ`nZ"~yCiԻ4=# p-hH@;cpA෩a/Dc=qǜ*a"(˙fFx\:);V}O>Mly 2e;[X>Xjq{>}[בJP\MܝW}~V;8 եxG;'qd!*ht'Oy =p?~ l#-b*J F1Pv-#]%*`NׯoY4ihԨQzUfD=Q2 +eΜYVJjnР0@E5#Y$)Cnz:wo8K@{)Srw$ (=": B@{EoqiFɨmjFfԩ8ZO hx4rVH %{)}w{GNޑS (G\ pC@1k;c(ϖ57=Ik|)t5*V2#IDQd)q!ZY]tsA3xAh֮N&&U\P/.n޷jR%M`nŞ2S[SmY(H khwڤ5Mm?ӛwe6L_#Wl|E&@eE3W(yiE}aYm]D cb;$[t2!CƯ~.[v/J@P26)M{rl^U IF7SPO9L GPg1^k^J@QI6pwZ00m-@R|R2rzu׼4(F$M nx@H@Q@}Uuu'{/*>_f+(Z[>zIq 4~@o/O qYҘh\I2ۋw/ъtF`F`F`B-L@ %S7}ZwJ&-# b*nWfSPj/] մ&Ä[7OG oEs#G@9G}̻ydU{Μv *T>6c!*hYTpN@Z >}ٳg!P<Aydztڵuu=RT  kGB=zP޼yeUPAY&nܸ2i`qי3g5/e˖TRy f$U{d9skDg (hhl6lؠG#$G\!#FPtd3۶m &X5^f%äNZٴiFӓ&N(eBB88(OW$k/ĝ}X@ (%khxRS{isAZ&-o ^ӸN-2#ixfR7>_p80‚q>&1n9ټ="ַPSH Gת9vyI*Gɱ+e䉄BJ@l"b|{g (ҽ bTE[@pg"*~W(M&-,x}F2y=y~T sf:EJhvUJyRS{ IPg* !G,걐qMk^J@Q n%uJ&z.ZV1cwkJf4*r@-6ck^>h$@ypj4u-{ M\E%>K=}…ʥ5)Mg$ %:ދkvĦY!}3nN`F`F`F #P2y*D4x56ō,Yᆧ~!X%x#ozkE=A1^Jaj׶,s⠸iJ@1f0ۘ3 -Ǡ$kN0Ɛ@@ɒ%5GFWi3z{K(=CXOCa ǏEK aGxe\gZT׫zy3M<&a0P̙CU](@=0u8p;!*U%MTa6;+WJvR*F… u1P0ixGaok԰|?q/$ŨYE4qs'̒폱 WJ@y~m:ԦhP rߧhC>z%\jgoK;z(ܳ %n!PZ(2*%^_CvM׬2} jP\hb@ (6mƷ.㟾|Cm&B@6\{O77e92P2d':zT 80g (n? 1^$(ߋ>6nQE+,}Tl1k"*>W(bCYk#KVlj⺍5Q7:K۪Ȕ5!sk4~n'PZ/DE5Vyt@N9r&'K'.Cs׼2lV:qġWR5ebn=q9z#cRPG ˒“jI1(EWrzu׼v؅H#ŞK2wi ˆJ@ }O'k{sIJZ+aYkAI@AS$䑩Nus zBR{/]f^F`F`FC0%LР`†+P.]M-ZڒI%KM=&gOKi; h֥K*W[Բ(-# *kNIa}a9P!~6˗A{T6|>A? l.]]nؘ,Ӱ4WSgCx{jEHX;?J%M~=pk =TʟN@ꃶamTp-o@ ZYXLj2׈$IGf+ h.Ǐ۷2P gL{֬YMZbPg)^Ltu4"]W\SHzliM<&1 +WJRU9pτ4s!C6O)UVP^i J@'B9\#gϞkM]4Gի[6 iA@sG)3JcѻL$3lm( ½xĎI'o-zyTwF='ij Oe͌%Qr'.Ebd,NUЋ?;=nKOʁ\iQꖍ9(xx l3½:cDo3$j7B@Qȋ}CƩi& PubOiPvB;O+.$^>wPhtwl\hpkrLus H(F+3#5)euz1չSpdrsUVuzpymU{pBr8=ES (gLlQ _/d$UT nz~fz>}EwOX F#~H-Vy#0#0#zJP%T֋5 ?PAY6y9j֭Vj-5EdSPcGd$?7o>5*lcS=0*Q]R$f:\tS (_\\%@fj۷U#w)FLTU;6ol"AivMOiIF*\H"hVfMvju7nAn:͂*m{ r]_*׏rl5P0J*Gs*.|߿%=VZe7=$%*^Zb$2(6E8̝ k*87I ( 65:"hPҕ+.;|fSyJ2͌b,&twG08;6ʻ2[1Zd(&C&@*0(ִ;P/_EWڒ6uBT8KZV@AQ+P&F$37ӥ;P5Tr/X!^`"*]g (PAz|I&1Al.IdTsQ \ }mp?l6q XtT`JØ$ᮦ5.P )Y(`Lw:q/o.(.}D}=qzuʎJ@n̚1C*HS (gJ4}j%~7GVTԾrQ ҧa^IsSbdԥ'j2F`F`FS0%LеD:r59PT 쇒7AڠAYj( u r4M%0`,ڽp[2@/<}u&Ɗ]slBEXbPBa)'̙^wWWRfõJ@Q`; -k?Jhhڏ%N?Kkb(2PTW0 'Dc=P2S0sUӐ!Cgl*eƍ4uTM}ZvՌ6ΦMӧOY xW( e˖Mۻw/5ʦnD6lӧCd@U-?R&bĈ2GtE=_PPTbƭ[}z@H ?j{M3f Ȯu\ĉw&O:T$Z3[H '>m^R|N~ F-z/* Ӈ<'B%a TkZ:ӕf"©/h,7f֥7U~e)CR˜sz0P6 ܣ@"z;Gr lӴpƫ e Z+aW(*Abزt9YmV:%3۬5t둏ޥ@@U$U)`s-v -HE: ̑+MkPP) s }az)SWҥ=@G@Aի֭L,٦B*x\ԧwCMgH4&n<胙w34OGuv3fVcPPV Dܺ0 :Ϧ֏UaV:}ו *"s:Eݣ6m8n7*UJ3f Vq_ 0޼yC T3k 38q욺D*Uuys[J@f(v@kJ@#ժUo .L;vQHQҢE *]n~gN-5jTI*3= ȼJ,ϟ={TWKpG@YPPVju;{޵k*'Q@A;wNv!HscX9ȝ;,OZkfiGqoOdevf]T @⊩C%Gt0ңG h,%yeczL7nZE=\'m^҉$ ֗u&:H#М~Cj955 MmkQAEܭjf_ҭF ʙ&lD":Q<ڰeL{^swF\!r̸a:*@z w,5}T~C%wԹjq%\-~iSɂ/>iDy jJ)kwe[a0\8MNL2Pܱ^= lbݭM0Pn2|;x8%;([ՠ8_~?to%QlW[W $>x' j. Wԥ^8Y1#0#0#`J(Y=7B@" ޽Ќ{A@\oWS։;'mKW;p ~I_J%h .Cվ*Ǎ@Y2eOí@BH%$NSg@@)V̢sz>(KwENI (Ԃ*`pu%]ԡre-o#Jyg1˵uac^3dɒرceV+I^t$`׈P^k\,XaϠ&M{n?eN`>| <\*4i"]D;kS\+j*;wKM4$H 3PJDJڜTH%~ȼVPAڀ:nk $ ۷oSv֛_ٳýv$w@H!LR-Cp]a3\`:ŊׯO[nuH f $n!A?8yެK sz*r]VշL3=P/u^ްN^VL@T:o?݆oXÉM:S4)tJꂰBXs_[{@M@ٮ(?x&:sӚb66 S̖O_)^1OH{ܪ*սP<‡nxZ(5(?**uК9RDְ"%B(AF( Ҽm˲A'B@|`^`>{I2QEJō;߂z(V) ]-UP>1 I1Gol짮,Tx|Q%q5 (%?C}ByGF#qXW. TOKn#Qս~ 6XT)4P2%LCJ GK2dF0#0#0@(F (hzO?ȭxsl}rc:a8#{:6x^%2u1{[4`~J-2}Ҫp %Uĺ*Vq?70j6n:%m0V ( @>8u =zLxZlVȰsP|C'G~5nU2e hVqmZW'L\xƗ6lد5\4a'>hy l_7"0tC*ƕ2>.!p4| O{}Wcwź lB._1 ۶mXdOpPП:P"E=|l"KǏOYdŋ{G4P[7"`˕+W4wBx ?rHڷoΚ5KȫWJr$\F ~ڱcPʊMժU#4ک1Tҿ+b`YlYIo?;yE9obĈ\k֬GR)#qƕic<^)Sz*>+W -Y_\C̘1}$6 @ƍVI`(ѢE%JQԩ8}ѢEV s5j0R@IdȐr%̙3iݺujVZkVr/J׋ #hȩ_zk~X$eSYO|x=>޽|.N@`(ҧ_hf#Ec.LoYo9x>|7J'T}(9UĎI'o-ziLI%GŁ|rq %xFw@~ (&G(B7yӫG|O+%AVpn*'}딡L-cPs$.hn,@T"~U0MM ks7qmH%OpŁ} n&TK/6[ p]˘Ǵ|heŸ|~ai:EQ#k]G(gBP8/-=z_%I)ZdbuUNѭ>Z<xS/>qrPML4YKQT"3 dΌԤuz1չg4ُzJ#Ș1Y"A(uk-e¸IL fhUɚV% 璀+o,9*\O@$U@@SA`˱ j,HoY~{DccF6,LJGuG͗435~ 2TYԯPȌyE@A}g9$w4TcqmpQ 团)yOPP$@iڬ;wN{ơſcx1{FҿwWF`F`F0%M6 u_%?h/Gx8j^O3# Tԥsmmy@0B@A~Fh׶wig~΢t&}6.rٳ1 ~?1'aBm˿h+t<,YU#v u4j<{b'_*W+{/B1%CFlcw;vr kfM,L=86^ :u2g a>| Wĭ؈qBS.qT͛GP0W^.u8qb{ ,3gAk鯿j,fm nz^?yD 5M%`-k2ks׮]%ɸP@BFȣG_~q~Ϋ^|$Y(Bǟi{bѝR\ :^v9Jlh&ۏukl5'T@&Rm„ 6/5aŐ!C$Ęfv%֚Y_b5[qX45NCOB=7ӵ<(UoCZTn(zzBrM jnStg (8]Փ&- -Qچ6*:q XOə M6AN]sh D(tQ"[6PfTa\o9_Te q%+PbT+'P\~W yzƵl.bC ITy$3[v /X-_p%&Ԇj*TD\wo4vZ6V9&ͬ6"@_?OcYPH6q:u*mܸQ cD |}} <ʕjFeJ Lhj&jS)zVi(w4"5m.p.0llrڐ;U ,VTPFߡ{Gu`P<׏@1I02̫ZӓLY4͘1~[K.Q2zM&KuءTRݻw%aCU ( URWT۷7fyF:ĺ1{ue!c(@hΜ9vs @#KJ$սN߽'e扵G C1%m颔;'6}Va@EzqiU@9~poβyD; nu\%$Ε vk!M)% OцcI%h\b&$,P_9xTBI%H*Eѥ ;dџySd77Ѝ󧯷M(~LoWKn]̭GͿ:ꅼjr ?] wh2dN{6&d5?(S`*5FM&O;:P {E{AE* M@z"H/E@w#-HGtAB|sfnw\6%Ǐ7}ޮU"_'/ D+iI(* |\qKnH+%]6gZ',YIRK` 뀴'E =Z?x;k بnJ?6դ)@X-uMe`yLav%VҲAD_jQ7Tf'P3au[A,è'O_g/6[yWX9Sb6>_wyl( Ǝu*#,Li$ kS/AT+I^Y"m>vM*SFa}y2¹> O[yKӓvԫ*9\٘7FAfuV"i뒘}A650,kRJnQݾOsBJj_E:9qnKWֆl[!gEjQȱn4wf~=dž薭aiGT{5 _9ͣHd 0k,ȗ/ z:t(X+U^sa1u%f,*<,:+VP $$eʔѣG=& QFXȓ'pW.nSe@!]\\tɮ9/@Gۀ@NJe~DD5sYpPGҥ nޢu_}$XI3QJPDi)z4=N"~(=pE^O$MHɯ  OtTz/t+rBPkEo{@&/΄CF \ ՗(p;F q44[-\KA VV^X{@/_gɵӛgBIQD !o`F`F`F "Zs\nF Fz! sW$ sԆq>p<]?zҸ擨_~NI˞Q;6|RzGɠ$GF{B%J\Y˝cb|;㖨hI+@%kK>2#0#0#L@ 0#N]ץK.&Mimeɒ%{};˗7o^j>) @ OaJxϊH"EE@qk%(Rh{;ſtF   (4|fzd0#0#0#0%G3# …sQZ(G /^ ͻxfZr9S:wL Ϙ4impΜ`L$3S̸q7$Iɝ{tU:8Kp9ʽ7P˝JSI7ɶqcpfmCUFxB+&o]C1߼l)|ܔ+c*J7xqŮ#+?`F`F`F3N`G Z4" `F`{s`؛'%-eDx j%3c92#0#0#0%F`F`F`F`F`F`F`F0%W0#0#0#0#0#0#0#0#0aPaΟ`F`F`F`F`F`F`F`(Px1#0#0#0#0#0#0#0#0%F`F`F`#x2IDATF`F`F`F`F0%W0#0#0#0#0#0#0#0#0aPaΟ`F`F`F`F`F`F`F`(Px1#0#0#0#0#0#0#0#0%F`F`F`F`F`F`F`F0%W0#0#0#0#0#0#0#0#0aPaΟ`F`F`F`F`F`F`F`(Px1#0#0#0#0#0#0#0#0%F`F bFE*ODqkT~,#0#0#0#0#0#7U>Wj,o7z|>P|BBS1SܸԥdIFʲO_}YEf*RH؆X@Wt I])QxAҼ|5h80H0a<?.]@zߞ=PiEQOP劕i˖*@ANzT3o>oH{/2/0|@ʕ;vfA}TBy2%z}(eI|\?&i/=}ˆwwDA~W&5Z︻!WK!xtUFYR%2I^=w Ԥ̻Ml8p^%}OXͯ\Q.~rQds6Ώ`")vyEFӪ΢q珩ٲœH ;"`JDFuh v)m\hΖsU2.WÇlyzo^_J&LףdڳF#Ft|Khng` )iY|l-e:W-#9)z7 eK%aŊt=#O"|wC4m=;w5h"/BuKwMÆ"%|I8tHkʞ=CL?҆ /"I4=Ztx_&^0ERV 3A"stIڿ wJ^m۶RJm޼&L^U A1xS_MZJO${4A[c*On4u>*@Uori~}26:r?J&-|ƍ tsp8@٨cУd\8ijZf8Nj~WGFRJ)<?|HZ-7HIo-tgߥheDVoONv/uySfo2<-]^|#AWK,ݗL@*@ٷL2 }? PqĢiDבd@J4Gp'ӢQH (#$IfV'~-BR'*T-Zg9ru9"q!???= DW "sVR7n_xAs̡UŸhU6"&L%KఔbzġTch[C*;i?!@K[]ZCv '.d7cjoUWByRzÜ|8MG (:Lj%>"թ|1%OP1 X Rdps4j'v (Xa ,'aoo7_}Iǚ`ng+(/{A֧G<ƓrPP.seΜfp(蘀"ȑԡ83Aٓ2eJ-;w@ ]#(f2IDP`eD :.PJת Z2{VoE%=V;?~*Rɒ%2}z"th_Tq|ER36+cbLbW :xV  ֕dš+g`̨XWϋ=6ͭ@Z?Kj;?T5mT8qzGVqƥy 'OPzM (^ϟuceSϞ= Xg]L:5cfn/%!'oW ?mMJ$7H'x@*Qѷ2˛I 45>(_--~PSj%-zXq#w4kX PL9i4A5 #&F:Lj%fu+@l9~a$?Ubw#j;iGos4 (D\[ niVvTuhvu4{vM~i԰9rzaŌ;ÊP|χ7f:Ǝ $}Xuwrźr&o0и(| (BFtcJ` D33^uNʐ!ɓׯO7^yǎ4rHO|,fce*O;C]A7w꼝هK^vkQy.NT_|P\W]'I_~NIĕ -q4|JLm~y=)ЌnY-Us:קZȐ3 l a<^ Zu,͇7/BR$N#(TL@a/)}n3Ԯeq6T}Iu|a%ٳnӼL@qnL@qᎀ5+ʟ?e?"+ *|t #ҤEˡNq.P_Gǎ:uJSrEm3\M_+Sܙ)qFYƍ_B۷rƪ[,={6rfqsƍ%5mz= ̕+3ʘ1#}ҥ4~R:|ˬҧOA>HI&Xb63y.0C=rݻo_ΜWφ9 UMrQ& ܟܾ};WҬYE:uR7,ϓ'OssoV3uUCXy߁s {]|3*hѷq lVgAoD{nܸC]M‚ ,nb>GOSݒ0v݈kDzJE۶޽ɲu4gr®>oGz;NI|@K E-h.I2+j7<~XgeNR&J Ảw>7 1Z ܨqXÎ:SZ|&wO[\g }hίa6B;m7(YH?Sq1 L7-yX3(+lIAGw?E^*_(7%SN_Is>NUX3ⷆ'5؋lT]z[>'}qhD~_r96cgXCV9JubAȿ4knU*󿜔 ^liUYvԫ<W27p4Y6fb-G 7o5IIlC;뙆f=lמ^7z U]]/8G/4'kF L@ o(2iRg,&v+b JY,0YZP=ܶ^ gB_1 Jft8N(D'+W xb4h8P*PXawS  Ii-VQNaPʔ)LݻՓJO `?SbSsF]p&t^RFOU(Ğ=!۰!CJFAjp% NwxJ@3=}Zl8"U]3!ޫTڵz" _AN9~"VZQ>㼄 YPtAdoԱCMs2ڊ~:|ؗBIt3N~e)T'\X$6mG Kzen%GÆͷS O^6ngT:M/-22:cG΅ WƃTqχ^C3*]Ki'a|ܹԴF',u4:բJJ%#P;A_2u-^Y < Rъ+`Iz m ĉ#ٲ9P>d y`2͛?tFGZjG;# yu(AKA XI@@l)^z7a(I)[nKcd@ 0VO*WlϜ2h˖-#g_6< ]A1˹M CqREJSQTOnЛuu}[(LZb]YlBDW A ?J7xl.C99}&*/K:QcJqounF\* C Z!Bi4O*z<7P' evuA>q%)Bb]8GFY yQA'9YR% O^2 J?$l6 5$h_ÛV1ED'2@^|CB@.>P~5AH)>2EB9Ҫq)tȠ|z;ɛijKe)C@7Pyk+()g|R@SD9s>Puq$@U$p}@P\EBp"F gX_)vvW;U3 J}'.iyqX,BU/C뉋U֪֒c_MZ,Y\8c{*Kߊ7(,7@9.]r"cE@rŗݰ#\Xj K( jIE/ZI|YB=ʸU+x(W*uPj:q T]%syi$0O$X3Q~nEfЍj] |_lB5 dņwP2#4.x-$OhA@A;\h!$XH22mV> ^y 6mriFMF/ի #G2?wCj,)ӡk'GSvH?FOz/- 9sE~e%sZ'ð?Ҋ0rqC0H Y1  BQ։ 6U X9A>ʪ,!2|O\ (* }# mUz+]jْ(Q<#QncWJiEŁ(3uiub1f\~[ZaJ'JkhP>nw#qڱA xQoY!t'qa_MeFE3㨺4ίPF DͅxgU#8s zk@586WfE XaH 7Z@RSN6mX0@ @簔@& ֭[A)G==cle ]!C3f16㑇N@Qe;RL)֢CJtիW4LE1b91Ϙ (ң={&7 i߾}F1WdKm`axQ@]t ǻmƊhopUe˖5,?\E9u o@pÇEXD{YP0~bU/w9%mZF5k\ZKB_Eե}AИ1cF#rصYO8_^ԙ-U$!!Av}ʒ2NA1j gʫ!Sf9&WyV*_t]+4`Pi+B Q7wk|kxJ@ő$J( TMt;ºGKG/D;_EϑBAAJz(f\Y@AY $%G|y N4ڸP(u3e4BV(O }cVCyωw5VsD<0YIBʺ1ǝqKγtIyѪ}vnT 8<Uq` Ҫa VIhίvЎð5u *X'ZkPt<\|TYa L@iIQ;WUyXZ t:kԢO(X})b +0ڠ-/ b E'<@0v88([<o(vWqftkr!as,^ZQ!7JYz!:#,myTYVj1_WN1*~;^q6a-LB1qX]{ <0VBUO^vT0%4w~2 _|Qjtlh!bV~w _H>nf:ut( m`RTAÍneL@A\ "1tHk#g ,f,# _C;A1rsa1݇_X\j6œ|v܊0aG"skԿ+vo\E;͡ ppS2Q&ie1gu4{:#L@%pFn5TFa?5.oFŊYUҕiVHeǬ{Iw<"\Lӓ:EHpI7L=fFp}a1=<~ն:U|r] d?ݺ5D[qx *F B>a }V@b. 5+c@O(*/]Mv (ЎwG>q(4m`d wq iVjX[eaƜ=~l;+=C3:E|@&6 ~ܰaCIQy TTu+ *:B  )×n26_a_O  Rt6)sQDk5~Q *~ XAWbݭ'.JTyS1@A"eO (*(2:1_bKف s rO?.Sƕp6A.SE|}<UY ̧#uxA (B,"|ߤag9A_c/`#be9DFRLj[C چb MApQ<I _{[Z&L[}NC!M*IU='~nVlOmrW>mڴ6QxЊWSs%pe}m 5]%BѱLSt|V44{s[@M t#JJU%V2(\ 0(A{#Z RZWڮrqz?W 7U0, u7󄊖GX#ÖlrE \4-Aձ Ti}Lόy,I@ Gݏ J G;gh8lGՏp5Y>bBÂyHhWoۆ޿@9{tqf ȍJ<%@V1b M 6nV, ׊ yEQ ]ěo\ѰpL0;_rI.]~s&K (րo r]2+qmG%gv= Ǽ8 )e (/.65 b~iu !Uܚk[Ni${C`9+k<-+ w6;fadJgG^UyBs (VV7| @` Vt (P~u<`IY XP W4`) ~ V,-CdK"Y/ҡM#n] 9?wޒ USssw nn 8lWXnqIk[%sJhWoFhaLm6 Z'0X/C?P}hP2ߜ̽YP!!5WJ`=ayReN@|wR>k?$naѕ1w6UPtnJԺD'յq؎jW*fU#0waOٛt~5Qq_Ҍns L6}5͞!ߜٽO7n=ѥK-*S:^~ 2טn{vZ?:;qpύ罚֨ t^2dp ʝ;ac_C5?_LRӘѶ{ׅ 7I˗/@;ԐlIV R8ln4zvvOԺfYOӦ U/(h|K*_t1/a܏dO[ҁL󹊬P5lPf9u}w])UT]{m7 -wf]_xAkw}&OLcǖ=|P<ő<t{cG[yܹ%9pݺu4qDK(A-[O<jff7n\umjѢt{'O~q=wP_x1͛7ϘEn{ۯSNrh"F*홺UVtfS\~ΎuTX1篿aÆTTI͚5kM}ٹsg*P~ իWO?tOWx/.w?h̘1.2d}2͛iرvH۾};|? dzϞ=^O5jԠZ @>J-+[|>z8s|o6x>\W=tleBAofғߒfyijQȶ~n*xd,IjP2| M^KOWn2艞ʔVn^u|2{dԽ o8w-[ 2V?t,趕P eĵ;iѳNP'T)͊i޶NwiZn{8t Y4/MPߧ37G~>%uRTWd|2>J&:qoy2l10vL٬~a{AYU ʑ6ւZ0{ qWKxiUV-SKz4~Z|nphQΒN;dɟd(shW?Bӯ&M8jR#jdߒВPOKxݴitԠJh{1- iXR$=o,ס'GډCH̴}z-if[,,{s3Lm[F1G J"7HoCdq]N.ߞMG/pjAבֿۅ1{zsQV0ŌNcZTӣ[[L>ӷ])V_97iLqv͐"UZ"2Wh/sۮp!-nψsˠvPǂYњ{֐걻#@Q,aaYR0impcF/l,iժ*ΕArcfܲ0:u*EuKm/jۗAJg}j5mZ1julUS.SVHj;׬E?lPE!2?>.b3sٲm4fRR(To -9$M& 1X.@1c/^/c 46 y,Y25j3 5qU !Xa ׅ),b&TȪU7M42ի \t?Rld>pC~U,aY9s&TʶmhȑEԪU+qqW+V,>}СCi׮y-A\zڵ jy1*nu6Yol[Wo +S%KB`É껯KRԶ]JHWf},&T:WF#EHrcX cBPN  PgV8}>+4'dwsh^^ 3_'.Ҙ P^S&CTԛsȅĢPOUEEU6\ q{׫UIU8}Dq|9V<qӫ͡Xc"۬lAfe(+ @:U֪݇nX'8'7YehbWl>wN/Jئ9k-͘a{Un)jؠ,Bd O'MfϲY9%~$ծct|eoJSeqr^ϤPiL &&\ςFp`"euQj%VsjѼB.d81~ժj2Z+O[RZwg8;ΧNf2 Ǎݞ2gJëeAo\/YSѽ\k%O[xꫢyvM b4[t4bx[OyͬB*ЕDeZLNW#w<ř|9,*]VZUCȲZu͗/3 weyBԡmėOՊ1T~5l-Cq`Yl=\' ҠAs*@a1>UOTf˚NLR'%Q3h˖r?:~:ouM+wU1V_NRTɢ'O:Z<0k܎ B'ҶBcKfl]~ڴic۪ .\0m%Kիok+e„ a-'iӦʕϐ\3-Z,ٳԭ[7nM9Z[ XcK0\g W P_͚5.ǎ޽m#G+@I>3iJk3luT,m{*@a'RHSGQFڒ)S&˖7[ o(<,Ύuv襰,xb6spj?&8y"viY/91##+@Sj3\jfBr/ Ɨ}4m{25iUJdUBm!@7L:,^^IϦ=~&[Ƿ.a;l'NdB9˂C*!}aaagO*Ը?s 0m[}˶r[XdbRܙdsIT 9ZQpaje:kCٸjmXFU(άtVrK.5^,Y+5+&d"8zM9Jdeء@C K'xjfC!z;x \Xjdb͊ VC3['qoQWcٺ'hޜVuy2K;ꤵҗoc\x[Y1ݸJy=}6n+@aŲgvehϪU(,>a1N]Q˷m{*@zUǮQbp  FvDCӬLM%gju꫐Tэ*@i4r>=W~IcP'\ P>S'&!@1usJx-~x0c l-3he N ʶ7"] D\.TJuo{㰕}5[Ucj\C>=Yzr͹a8 ٢ذe"kY}9LsW&.*$>n$,i8Wn W7wpMcJ8\gwkvYJ+@1Z4նe\T3WCa-@z\Z .VY::} O5UЎV\V7WJUbʇvyؓc4{G j nͫi%thECk~B5y?M~IWZ\j}at)fd:qj>b/=YTnڤ|n{ʗ+@sg֭2AҡT|A&;|f9FodJRy[WIeY~֚rHn'69QcFZ5eѽ&WPTf/SEXP僤%N` ,VbUKj_B݌P>}Z&T]p+!wX|hu׮vK*1MCƻ+@,9$b1mh} @Q{PoM_ PSoU}ʖlԩS)vl$Q0+J/l^-۫ P֭[R2sw O4+/,ΪSp>͛7Ob [.QCܸqEz3-]h0w3ȮS1X-IjPf]JH]L7U ~!1.=RPy/ fV6~mfq'PTsakS8ŞNm՛sÊqX1z.U !\spW@DY[Y,vȢJcP]Tzt& 4l] ?S]}\p&h|#U-+}+9,+3hߗ.&OIyӧE #UkkX>3KSy=ipXMq؊a-a8~Xj,[byӰeP"@~6֬j 2ftRZ&*mӉ_LԍҦg'F_5kqʐ!~8)ۙ]BD|EϩU˪2{Ofe4w` ZPy'ӌ+UdEبHйsM*[&E1I7˜%mO(A>VҦM5Rb}'V͒r…Ԥ`5$MaN 2uBO>O= 1,4ZY*(3gR/MfK">._k|AWc",(\\B*=$ð[QΜ퇩OiA)IRtA/7~.,Q\pijmVPEO؎W7kU PN>MnWTl(Yv-w\hoP"@'OߙҧYc:%6pVv6ʖbgC/[{5z0a˓l%ѣe@u?.`tS~t=[}B|Э=UC(Jr^к՗Tj=(@_p,\ЗؚNNJ/).'] XY2q J۱ė%M73m0cĻ1^RX̰}d+J_^s:U-*y;f䖱MGreP"@?hbE-U:[{yV_~YDXRmڎn“tf$vh㌓P'9MDžEL9 Dqw*^<}Kn~;vWe;%NOY/*6+Wʘ(SD4m]BF?jٙiʼn', %yb-Lg(8 ]裏l?8x[a>|g~nMX؍ՂhGѣ矞Q|$L<ׯWFK ?o_P uh.(<.-}!vV(= Ea]Çm/s;w%,&oy_(ܶЎP]+¥T+(|=ivի}^$ܟ Y* Wz",,9ds8w3*qgITYH;%&O\'%I MgŋYDʔV@|h泟<3cMa(ܜYNk_t b0~MӖV(~k3VҥÒKv ҾzŊՊ~)VpyF_K^zCm.se> n<0v*;s5fVW'2+-v4fe1VV ? XXƁ+/=:,ޜVê{{Ѓwz͚&!bGuJ%e`}SKph+,p6WE!9GGD2%Vل׫S|=q?'~qd6~՘̻z Y 1%U؊)3E.q!@1r Qbi[xpE*q~'[$vn-Bw9n~|WHXYU4eLa{檀'}fahJ=K({)YX4K? nսъq؊>cWp*\o隆U=:,ޜV|}E ,>s^lô'^o|=j6xg(>\$Wܺwל˽P}6W*uPܓXljs ī"@zmQ|.g3vZ){/~!K_Y$ s؅Cukt?CyĽ^k5a'C&ʖ6V:?ba+W+Upy|f[ׅ%ik%dfҊq؊aRm+7Hcwx @ ˓?o&,}״ ~Y a%@aw8,ĈGoDЊ gc~mMۑvg8[\jyKPFq~B2sFOb ]Ɔ8:zt{ʜ)i,@9%^VmQtǠHvJj_NcAM쬆j95jYr]@ǖX2ԫ.SXqUcKM})8pZZQB&[hF744]ͰY`!O; 춧xvɞXы g+#,)$@2X@OX|-$IDZ;)S&ׯ^ҥK5u.؝f1Bz]uAc:t^F!>^M߽{Zn _:+B-޸ܹs$cX—^PZhA%K4r{ӦMҚY&b"59ł7WE&BW(/֯K҂?/%$efwZ/&Opɗ@IwQybrMsoc,' k+BɝëNDLsIy:YLN Y0N<_IU[G2ǓTX٬=j\Yc@p +Լz!`q%E PS~OR7n&D,Ղ.ˊq˱z mr;8X%@qF{g+9/ HSxn5k}K, mWK.YS0J xD.fW;F]YhMgkc옎B5_tG-nT|3rD[afVMMda)dŊjݺj-SvS݊{He欵4cF7ZڵƍMw'8sdO2fɒAҪ/e }٬Y%z1 [EaFlQLq#*g9@|]A}u5 m_] < ¨|m%iq#Vv-&75/ M)^IrS*.nT lg{9]qٹrǫ{kɯkOSM|,1 2WZqt:v!ݲ8{6eѣ8|mΖGtp=Cԑ#c1U#SL'Ӧ,d9syh֜Yy%~[/Av wY[`K&ҥݫhy'NHٛxɓ,[I__e _MEܿl)aȑ~˦Y-:E 5R&Mh-/a.=,aK3`6ߦM~1۪k׮ԧO-]=,L駟,e&Mx2:w1mf߻woa-C>}vٟN"3:к'U-EBrRدZ>)vKZ\HKOgW! PTڊ Wև8^uI5R,X, *Ha_s=f54i5ғ?v՟ ڦR;OYNjr“n T c=G'A@@f(WUGxV?Wezf_AR@IDATwBBU-]tXh>~l Yc?xo٩HS|ewfvS&w&{Â{KK/2B{orM oREykx= GsÊqE J楤z᱌W>CJE1a%{iCerei,kYIk\*/ޟg3LAa˓yf < ,>3d?k&5yq~G4,Pa1Ϟ/T[Ƽm 'fո\Z'R|g0tt`\mi`@}; gl}gҺz*X%\j,;U-&GΗVx'ȖĀFYq_vy.}܅N?'MXuZAŖ'<axvh)>HI#ˏ{szneIm$JuvQp\Zcpvx,`wl%]sB¾,{$nΟ~Ai!&ٵYӉc3%?B߽~Nς"/x0|^H\>-aˬYܮ'ӧO!=(䦸ΞFg)Dx\>e]a]E;RfTg,ٵo:vYVűӉv}(_;wMT2ZLY±"F<~>f4:BaCbB{\?C2 ;lqA8q$Yph yj3iv_8N>]*wĸM` `%,aܸqĢ-E-*@a1ϓ'Od[O,3*VE+dɒE߲ӧO{tjef3fL邆]'v QPh6J٥,\rEC*tv"䢰"jɠ4'^'LߦBp\'Ptx'nhtSp_Acz~% n LD_ )f 8 ufm~ݪz.~WV'vW~'?O;N͓<΋#2H{7KaGlO5o=ͪq'cUKu8ߋ-uihX岛ԉ}>bFOW+U"nXPV.u?׭5F^CӯZ<](MV{q1c&<.OyNn]}|Ř (N @m'?f9\sO@uk3cZ)#@h$FW/~q+9,4Ź]ӧO^zn˜E_g=_߾Až!Z0 Xy$4E/:N28j?z5M{ޡ haL_/ '{U{9[-SZLQ,TNJ \(ݗ)cl7HcwG$hŏ;M?lbnOA_Q(_E-.T[P6FxrV>kjWJhqGtt֭[Sb&L@6lз9s?no (aM ؋KlGy=O<_AjZ 6 >z #\ug+u/+.~m Z*VdQnx`WJ K~mxm +"݊蚶pR9VyyJm&, C1@%׫? Py8^)Qfynq|aS4<|@>Q+`7H6 ӊZ xTi, CZJWJo%a_ƥN؍6mO6G' &f 6pO nӦ -ZT6IM6\XIe3O>z7qرys~W0!Цba*1ts-rw(05uR5OЬm@\"@ᱏp nʼnEyҧ"vŁݶtIfW[cl-p@ O@y35P7p%5k:9]e͠shF hW;={ڶE/A:uDI$//^L{쉨Z**THZNb',eŗQWxȚ5+5h@]ߧgsPx:v ]zU61@^Mz@\e uF@"Mh:e;Т?ʂ4(_]A?şzmWi&c:6xNBl{OkPyӌT>} =*E<⫎{ӢЊ;q$ A@čk l^xDž|%M@erg)Ѣ{A^EGiKG@\?+G| Ō-/7qa7$݄x }Kv&{dDAA@LiPQMX>aMࣧG4}^* ?@^E~F7&akp(P;    aON+9W+(, q`8}W0)J P|@%`               !o8Xl@@@@@@@@@@@@@@@@7 @ g               KZ(Z@@@@@@@@@@@@@@@@ ` @]7Q ,Pkq`                3j%Jv- @@@@@@@@@@@@@@@|CpF-                 @ خŁo@Ψ@@@@@@@@@@@@@@@(۵80 P|@%`               !o8Xl@@@@@@@@@@@@@@@@7 @ g               KZ(Z@@@@@@@@@@@@@@@@ ` @]7Q ,Pkq`                3j%Jv- @@@@@@@@@@@@@@@|CpF-                 @ خŁo@Ψ@@@@@@@@@@@@@@@(۵80 P|@%`               !o8Xl@@@@@@@@@@@@@@@@7 @ g               KZ(Z@@@@@@@@@@@@@@@@ ` @]7Q ,Pkq`                3j%Jv- @@@@@@@@@@@@@@@|CpF-                 @ خŁo@Ψ@@@@@@@@@@@@@@@(۵80 P|@%`               !o8Xl@@@@@@@@@@@@@@@@7 @ g               KZ(Z@@ot+N^yEԢ7񛶡!          C klԫx_٠qGӖ qh ĿԹs-IzF4=o#۷N2MTyOsRd]W젭[m L 2S/ɼO)'2%JDZ:uΝmQo~2,Z E8T&WF?ڼ^v_PؤT~52%M^K/+o781Qy$n'n 8z@ߣow?kkeıcnS4[ +8NJsm>|   IJX&ьM{ćۑ}̣?m8t.~(zwhR7Z\z95Z xi~$/1#Sd#\yDzܐ(uJNr"&,EPȑ7t|p9(ާ̅Rɺ..@#XDMӦTњ5M"b9(qx:CFÆBvq6=~B&]ҥ)OfY~ zMjTOÜ3w-]Uo)MU*6-t]:q͟^{0AgZ`ţ*dӛ&#Ѡ SfʹM|Nޤ˗/ә3g<ȑ#KVwmjѢ|uIBϩE15+S@n=~ ʬNu)һK:?);%M@}떕G{>}ye9hۛF?wfnͮ~O)t''^>w;Y(1hԷ_$&;oOjSba##ӠE4[ +J* ]=żmBlh"}|+vwk E  +翷a~-p/O1!q^| 9r!$23R 8+3jkfFECr%#.!e!_Jپ'nW?,*?C/䎓8&}0 RrSLDI&@ۧ1*dC2Bd=zX^Co2dp ʝ;6iѢ?BELn6ϋ 1߫Wi̵4oChѢeS+uYS.{jlٲԤIdw1cŌSEÆ cy"xG~Tկ_eyfΜI+W~2]21,'S2E53<&ӿ/l/\@S%e:[ $@Q) ݡu#|_ KGwC͊|o9P^K ܣÞ;)mkR(~^ȐBx "~;ԖM4Cj7 Xya~tcLoÂ]Oz[d5 5]6sGK+>kRmKpIj 8 XIza\6ւĊ[eas-v|R#(W2_ ~١o{s?P_գ[!j3qbe:. ]裏z(s\6*_թR%ל+ #vv}:u:^$lʖB,p' ̙^fkSD,aA;!q4iR*U,}?R;b"$V(jŷ6("P(e$QUxj׶}"E ʒ% թSbĈ_EÆ ӷRGm <;&>b:]-ز-Y9Vk٨g6+;>}jߥǵĴ}uطy 8gm 5{rĢMBV)C%[v`"SeK~oV[:Ol7MN *C]&u?ΫQC12@R<6Cޠű@qb$wt.Cǖ;s$>qڴVV PJW/FͿmRh6}5WUz9RLUeͳϠe/D㊨B@DWg}]6+[68]jrFoSi*%BGiƤ~"%G)Sacе-]+j }\kD @=lyO ,(ܚˌqɆu_י?}#-3a8TvvY'/iŘ]n@wi~Y1STpY)!:ɐ/\ Z5.t3P!aC uĔx:z9ţЄ"G* ;AWMKg}: 7"݇Խǯ5Կ)%NزCֿ悄3(mfxcڽojKJ6E^xIW!vww "'LwZfI+ +s<Ǐͬ:i%J2҇&n0W _K+W5kB~ШP UsJ4>/L>O CG k-yr:CUcVY@ɟ?3կWx.F65Msmpy3*2e";B1Kر Ϊ|-IyXNZ0tNiSrʠ%IOGp>|DO|!?]-tݛ2$MC{ d||lưq>={1n{D12v*u]G:wߵ… _+V,ؽ۷4meĉtQmo:EbƍKQDܹs4sLTo߾24iR3QիWiz[h޼~%[7絖]vµGrСC4e-olQ#~Yɓ'S bŊ,Y2yct9sMKǶdںu+իW/.v^ˮ[&MD'N8ޣrMjբDQ趯 ׯ__U ^hXfjըhѢ_Wo޼GY8s{1cFqOn%۷o͟?5k&0[|<7nl Iy<3g~կ_?}m.VլYS^<ŋ;ucEϞ=ē'Oԩ/ex^W9!c^4hn￧Ǐ;d㶴lْҥK+~޸CW k+(MJ'<46>zvc6N'|xXO@se)Q \|>Z?qܾ/?ei&/ݾO3.y@9}S,7}, J'qDk'/9G7"pSNJ!qcnp\X[۹Ik|!Me?nMq^O)}҄46rqz',%}.4kٻ 8-}>%FnP I, BABP)nnXE=̛Ï;{ss`ξ}~)ꙄOQ9 \ϸ[ň1'5㹑Ku0"9^S&SoЄUvU;WnUcbT2gf5Nзh=/cNJ]PЏAJ tcUծ\}ƍ1-LD4?r2gEc(Ej?x~[qbt̔1ykf޾G=dg'qjۯ-ZT"gf\90ykV:, M$ {Ty~_}]{Փw+p+YVa/+L%~Tkpcln{d07↓O?%*t)-vvw`~S }2cPPo|3*I6>ed)J/KIkg7ݧ3>ûsQk5'ZcC?w^wy}lo3S$19MwRN5=iAb8`(R E(>'fXV Z9Z0Ka IS6qڝ{ʭ˗yH3(EyY4*bԤLAJ _Oи[2b[cҚ,NNW-~5??V/Ei&SR"7J ;tyw8Om3uX5,К:W՜w"M7UV_oQKݶ}:Vbmc=/L Jʼ%R,y ݻe@gbt&J¿Sģ/]=sNەڼ^<~Mߧ]'C9KfR$q ˧o12'|3V[߯nyQz627&+x;͋wtߍ~k(Yڰce M[rthY6s{mHzJ1,7YuJ <7>s9qs+fWy\q=S6^ .2v?մ)^ت_`Нks 9pY0`l:fz6umy;nL`<1_R̍ y;)ft|XxUupAV0Rajr3-ҬY%jۦ ;w*+(І9#|\5fXm9sXA0g~5z#o6TL~s7~GdnݚP.9G G0mkP7+,BO OA6 `0珱1irV.0͛WV x< ӜR0z^G2_;ƌE2ַ>]U@K:+ҭ_ܷ3#B[e.MJ{EGEϓǎG{QbLjMޘn1{+G۶mF ךan BTW*Ux>`SLG0<@iӦ)`+'V^gtUW $8C=jR9T9b`Çm4w֍A%TRJ_N:tp zrn~Pܹsrۯ:8q(0@v8|qVPA$pʸvquǎԯ_?}rH (o$=>?XŅ7RD:t2lf% &cA`iqq/ T~(5nX+7@R]t`o> c5*1656Cb4$ }p0D4dic\̔a㻼6襟'Mެ}QrB7>9V6`, P~ݢ:Ab%\IvKZES~PW+;/9ӶR1H֛{~wY\ vl1&!il u|Z͵3P׺U ~8!)Z$P?7Ӗ+9`4? U,md?NX 8:|u_==/a/޵snQ?1X)➂n@a]Vݐ)F]3ExJ/a@/R8]]3Ճɲ$812!_iPRC~VBK30SŸ $7nxq )PL7P  0@ 4ܺuO Oa@_hѿ,~sWlYih f9O>ڴW; (Ru (.Fve„%͝+Eṋyaxnō*Jҥ2Z?&@(/\ PC{wy&(%rA7uAJԩ駟~2,`4i%}4x`c|ÍQ!(:PX>`N/(' + X`]6P#( M.o۷iٳg7[ŋ &EF-Zj EPt&xK.1C#Z|TT`A 6Y]%Ν;j~"&a  qXq `(cI\ŜI /*YH$,}8 ugIS(1mO#Hu ,cmP^b:6qu:/_(8m܆rz|'eMy VZ)Ƭ+":Z稾@iƬ5B 6JNHo3kYs03]*qQ"- |zc93B* 𥸊eG 6g6MaÌ,`_ ef/mehָ:1_WB:$EgrN5J鶬0Hßui0NkK7xL`[u-j˩ P wxNd̼Gߗ`d <ߍ`a% \tSh׺BQ,M;szϦoI)Bz> @`ʷiSw~[ע )n/>N~9:2`֎ Uj}[BU˓`5%i;|}b`҂oŪ0!of v#ðw٢w̠d_(Xs?`y9Ӿrqck%~a"=`bqr;6<boj`:5-)0ٻR2X(Al9愎c@HJ}NKi S{/іIvR[ @`鞔:%Xj;/0S $ǘ9<" l*[W)v{$inޡUP.m[`dbŘ/(̝(/`0Ơ\0\ @')2&r 0ӌR,Epy٨\{lkK'#+\-%H Wk niαy;.%`Vl m]b%`@M1㼨L gjv,fd>xXs^JÀ-EP5f_ n47>@g9;3KM[y~f-vU|H-PZ2|G%( +4L*o3U`#7c+_T{f&fڴN͛U6RwԈFc-k:Q&CZe4hiüS9Ӿ}D0W\@AI*V)޽)r-OC~7VϿk~x~y[*UʅXN|_pM~E>m޿jf|Jf&37̞^Ap*R0w RcFbr< $DFơ2k!}rTt1'§t2̤iFy?Q `h `)ժUS `ž=/lAyk׎ٚ?e~f`ݻ7=zT) A.'Od&W f ̙A&0@M̙3GU%EF@,y I]УMG J`'~͘1#mժU D @IDAT=O0&D"1>{aL0C? hРAFPpCK֭wGW @ў9PŎDCGzԩS`¼^ =^tw §22k[9mܗbS 8 A= -8[LlCqJ)anR8it)>PoP~۪zZMe-@G*S_ #M+@&Pu߶a@s{,91"c@[2v(1c`& IV3}& ag}1;1KɨTϽ]t|ob_Rɬ(+gll^6Y>vaJ%^oY Y(;|]y)fT X*(Ms`(gt) mw`8.mH f͠Uرz1>neW=}tv[#a!S[~2bh+=\z KQm}g27PnI#T9,UHHj*S:b㍭NҷoQn-[:قC獫Zԡ]J5'WO•,Og"M ox ̓x@ :?N .n4fBs0;¢(VR`ēY3eݺu 8%P}ڢEm9sf8pPdG RΜ9U68HPP_XP" @f˖-2e<`磏>2}ѨQtݻ׸3իSG?ǚ\e;!1NPCƏC) E  NX@ w}"׏Yѭh,(G>]u喁&w4f(Ƙ. `ByJ)8Ho~Ɖux +5C$VDYFPT30qğA>_ S'߯P]0ƻ U sl Z@Y So|@~=cus*CƆ{k2#ܛ l2 @Y,|?I7k05=I 'dbrTg0 x TP4B:|u_n'b?yB;=*I&6Fq^r{N[zY_-`? -J ;rv,9F-Ll[xD%0#Ƌ.'(w5hSkz v/xZq\d֟ vCӼy@e&+s"1&vU.[&1{hfһwsX=ƧDcv ool A|' @YxaVsXi֪UUj٢z;q~b-J0l`iJ č@PDV 7Z/SɒyTOW81q)v"#3v8@y3_ jvy4fǯ: `@0PAPd;l>@ fѢEָ(ܹyߟfͪYr% >ܭH9K` Qi.]JF<4ϓ;P hAlɐ`e]dS6M@ܰpJp' YFݚ$S8ub>=jCv)5J*%|..fJ!+A)rcUyYǓ}ˉX(2u( LA٫cI4` T@(RA*vaCޭ^΢vsJk_]jSu%ŎJѝIJ@6 @^ P`DP̭>Zp.jK 3؉TƙZ0M3<X#r^R6^8>{i1a,Cmr$@φ,o+M3,!̓2@>N_f2ĪUI|sJ$`mh@U]R 7+I0K3VDɦ< @ TdKˬzͪ)o;L/ũZpN^]{?&gYOon9Gfzĥ\R )|vg;1_WY@2δ)LYl`a'e}vVw<%o붸3aSp\Xj;d㘯'Wd3̀A0d\XJ03*x%YqH:*P'%4@ fiأQoV0d81P$}Ë9=]04(+fU| `(DP$d$b۔!`C `"E KGoW&d8˯bUJ 6TA8U:SؔʭHʜ!("Oӿ[:v+'yhzc:[T IO3I4~$ڵF>څOr&쟳ֲD9?LR+%[TȜc ;niӊTF öxbOWs ܉q)-̧7-t: @mI>IJ,\pp") dp/*~N+W0;vO63 75¬ @2uErd$C+6a,X%rd|@qb~=RT4# fߴ0_(vcHkvAVf* Mafq۩L} sM5_y'\.v,2Iw޸:2{'|53ya[#Oz^UY%M3ٽ]N?}fW~;N#MY)N|zFu\ Vߥwz{׳X@NH0W᫤ˑ 2#J#$9<_j9@i"@N9OE=3LPfp̟n%!ySQ/ 3Et* ŮHdPҿHK01f1Wl⣎32$ײ`:s }! -gܐ_*]ɇwy"bFv)I1cqMD]B%&Zt^ 9٬WL!?$'|3`}(u*h„%̞ +\'(WJ޼YhZ>'lǽaό(g#0@<@FZ$~Bl8,>۬_v"5 J &-+Vnoo_1Syϳh5Ft&U<|4#؉<,c1W?o9_,'@^S>5)R @STJz뭷 o;&#BrHʼyܘNd5 .Æ UV`lmy;ϟo ,˗Waǎ={Zƃ1%E7o .lȑ"Gh'O*PWi>P_(ٳg Kteq;wP֭@PЮh1b0F.9onݺ϶_]{s7h y bDУT K:<#؟~ d]J0IowRyeS0@7gf+nyuEoQ.p>:h!Ҭ~VݎN6K|noIV5tu|vP2ט3&U#5qqB*@( PuEly[^ʩ?Dn A߮VV1T91_W]@"d95h*^q$%uXcz߿0SN)}uo@-S3 f9AءabΜw[ݛ`WSY?d6lEdJч4 K WVfF b$(1oQBo9 @(N<'t @2O&Mo M"85_4eo*58"| yD(SUoe> R89_|*|1y >/i*J[Uy@ڔ.Y"k;1_WOP3g7o4ZXj!hI2Un[Hzb j3!GƳ{HOdP%C}hvW(O @Y3ZL+ď @2xEAnd5=<1d>i% ]7O^S,AlbȱmhmC`OL?5;3htE둮7@Dekh9z7k?OWj&Vf DV O @BQ$IѣgwZ>}~42Rv)a#dX ~At9ߺON8OGخK7&{ ի0L̝4])_PԽ'_5WJ2.#h_+zu˱OTl#hY٢e츅]o#\~Zs˷G7Jeh:D+$҂7;5ZJ[3yrpuR(ma\kGn:e̙sBz0/PݻStv0;e:LDu('OlYe  ܹs#ēOR % ˖-3GQ9s uUK5f̘;Z5jdiC%0@,X@0ݣIP$ʶ\Hʅ U /ۗs)$s 2eʤanG]9 @ K. ǏiΜ9ctxb-f3:F@@6ܥt}tͭlk-+ 9m&; )ϰ?9^`3f\u8& "ؙP'ŧNkÀ2݆$~\6ـz`@)l| LW(a2ʍ'Le8S-Q=dZ|89N,48%}O<)%SVBzO6VNd$ VPF.`_vkYw]|خ6A |q00 J`f7J |!iW ػ|ӬO$BD79Enޡ;PN+-ݮ-+lcL"G\x抵rg!WG_ͼ+@5:ngY{5J&|x3}%ˉ,2~g!Kt R`ʜ,@]^ 3+YY. 2EPLEF6Z(J3ղ-us(NWUn @1y~mLa @(N|%/"Mm}ɵ p" \cv@)X)+ŊbkNNKt B YdiP.;Oi @g_%O9f(QyE@lRFOBf<`Ȓ/5E;2H'S:l'xlwHۺkgoG ]z ig(Ft/e씻te0*1g'P$泴g l$KU*3htE ؖ ҌBn fs6Ix3g|##, y@&cjE`ҬY%juԬ:*˖o?u"(mKG^Q -`@|HÆw*;TtD㨑([6 @s7ʞffGQ ͚P%KHӦ+sGMtaGA2 n9=Q"K $:`בOByM dT$֭WFy JҸI׺TLk(P)/A*#9⍡iK XMnb%G;vMP,YBK8V(3M4*#ݛ .vE_}>eYv͛2;Odc%$JR23O ;PJThҤUU uU9P g5k֌ͼT`'е#9TT tE1+ċE>(a2 9SP:91wJ*Vdޤp5VO JɨTm @5f&(嗭tw6q^K՞Z<*dUI3f}0@(:O42$ ex%pmpZ*Xѵtef*V! l' ̓Ԫӈ+}BӻY>d.'%C*f3-̙oI*.%%0a)N{kd/1P-.Oi63Qى\dqjY'h=~qv-"av&M2cgFK t&i҄ _GLnj]@'zJiFM5ek/kIǯ,Y67cfpv 'eyOըCv9ߙ۞sQr9rWRJE pKf`H2O<1v$- :t׭}v6/e*RL>ݍU*@ -0=d"(DzHپ}{qFxP$ʓ'oƍ4pyC2f,Z~7x _e~bU2/dՋ=IP$0ٳԩS'(履~J9r䠖-[tLYɈ#[m ViOnnڙ0*@~c3{+!s\{`Wn* O7}Q:I%%NK䓔V"A$2ϝҀ?WYe铟? (r}ÊݪOy&3@`bo/v d [?MbqOHAD_E'-f+ 53%NWI3 ǰh;O"(PAg%wmf(0uXmح06+ȓʳ@O}"""yoOPu2 (5sgl9lnS~KI3y +,0qormL' LsDW E ȌwaͶ4-**Rjlju؉t?}mΈi0!{HNϔ2 }۪Z#߭v?_uYZhZzl1:‰l}#k4j*3h^IDp:m_|Y6&߶s~Vx u1 lI"AmXVR$6[ բ긟0q}x[>2POH5wCl{ }(!@)X1+e+N 0]X47(Gp?΀` `ά֫1 t Uy#нmb{@e\;wVNw0;N9T!VlHz< &4D:u :C ?; "(ƍGz+]4uEU԰DnP~m\Y7߁8q(P~],+vHJ&Çt90KZ<-OR^=q1vA]zE&GQɒ5y۶mʄ6c X>(T%@O?+Ok[oOBiP~l_R%Igw\wi=S#dL01 ?Vm3CcW J%fXiư7};}y:7ر^Ta(:G(EhpEֱo, 박`7fMndЬմͿxޮO8Yq\T v?TJ%>É~Y5Uq'ZYV=Hu!6u7ʸ6=|u_%cjcJ W N?j]A2T?OPw̘|R|--}TæG߇ ^`:/M ֨[04Y`G&NH`mdΜY[lͿ3_?Sn߾%ѣGԶm[ezDyZڜ9s7j 'ww;` @I2%B AIQ0G ZF Ws@c Ν?Эq7o^P0ļyPHB1 A>a=P? U_OYJθAˉuX׹v;kan|ټeN(>2(o{UQȑs K)0Q X#%й&n79<=ͧWol Ꝕ.KcnD *|͓)z}.m 90uGƅ[Ѵ|X?i :|u_u_ ҘNo{V:&#Ml4b'ŌΖ4+s}Z:+󯟿Lh?\b^Pt8ܻ̎?W{u}YIw}g&v"ͰܽjD.h0Waf:(;ӎC:MײPbm0r g_rMQtẾ @m6S9V]d)=VqBTk/8ch0˲]VIz貪T.Bz@㒥۾0:uNKO?>cDvQg[JK)6x0( l Z};\CFJǏQf"Db\BhOWoѶW@//&,q{J`ٳWU~Fzc/X1}7Ɛ )=ja\ծUZ3 C+LR4Ͽma S0ΝPX T3Ѓ/>`SL1L@ F|brJY ;yԩsTW\˗+dɒE˗رc3Зܮ'Ur i"mN"A0`mڴɈrǎʇɓ'8È`xZf yfZz5Q -:uJߺ] ? X뀶Z*]5ow_ t 7oɝ!CH$IMv˨QTfڵkSVT U ANJwri %nܸ믻b'Ov (t@"RHK={v$ ۘ1ch2 7ӚknWa5Jex@-sE4 0oEr=u<?l@(q`tSW<'ur2Fu3tʘʠ "NWk%m_m=zZm#(78ΘNM _8]yPdn>|ns}烲O0 M0\yj$ \m<'/R -VwƬ_^ l)S'ܯBoS 0$+mB,+GrʉMC  ̞W${F*J)6hF@E?+V b̟&r8ŀ"T)?+l .zl\j5N]axQMh$жF&9{&MH=>)bb4 H;N1cĠR2+V`'a|/bV([ӡ3Uf`XV79v@,YQ6g1tVlrˍ߯}VWһ~ap.w!0v:9[R&%KOkdP̌ x Ϝ]ϴ \sblHV4PM݀4V@qb[Z_; oGO{ p!o?RLF0CҌ1wUŸs2%]]-aj*M֤TQxRXo6o82L6E>t{x]r6jZF132vGPnHPM!Ymި@3x`. PGX_YUԲEUp+ @ǎdK9u:l&wW |n@CǗW=ÛY$Ucf%봑Lݲ" GMz5jMh&8phIz>3dp'7OhoFoUGEE{U`FJ>Ak V2thgʕ3*HUݍmJJEWfFޟ~+m: (H&+-[sc?QCklhlG{2Q&$*역?Ӓc,y„JӦM킕i(ĬFlbQ(%2@Ä 2u=äRNX0k3gNꫯ?9}?c #._XUtxd]%EV] b8 +ի{d?AZUWhQdƍ~IJ̅L|7 <KÍׯìP\k ~״fţVn!Ts^=05y5#Ж6FR\ ,N CaQ׌rf R mƜʽ/gRޑ@APCAI]ođ'qo)e6֥'7>kAڴ]Z'ƬUPAP"v_j!~5+GR;@gzSުPTǎ4N`U]jp05 (5t?WWS<Q7W=Pd<+7X1s@g_hVsj|p+{'Lc dV A<(;X(%eVn(2lSPjq p vul*@C0pyfzj sG; Ή %V>qh@ t916r1@]:h@Ab~F=mn_SN0Mϟk78NN`US6WA`YX6+/+~CS]~@nك(-KJq@(Xy_A#ZI7.u0ض:2IIEkbܛ0*(_Zʜ?nXH+M,7(] @i#&mOPAneE X/faVDOǴ82 mp=k2{]t\D&Fm/ln;8@f`YlEfy^(@$ Q>zK]`: |tf^SPfm 2RhPdGٲ{w ODm~!eRyOZݘΝԳ̊r2"ol"gpV/BO9k- a!ûP&q ap9jի[`'ڷϝo6T %zj{ΝQZ|l`4i(ƻvmLժ0.tak`F:tj' pYtT E72vm\cfUӏVEE4!e7t\\ܣH0w3{̬F:+3ڊyjyݺTD߶ϙO]Q$^ޞF֭[S,YW/В%KJc1<|צp kj[f37nܘ*W iF 9rMj߾m̺@Pw7?Yf:~/((\rC5Ov `A?ۗ1 I04L*U*믙A+Vn=v~YcǎQϞ=#|Wo_ NgS@I.\P ɸY]/N`IUNYF8Ӯ];YȜ,`;?~-Ӝkz{f^7efRg2>r ` oP ,yDKʱlzSr sMb%X eNP.]fU{ivg0 o-ASڭ ^8'5-W("yM^`Cf+7¬P'q ]JIO',tk!(̂ 3Kn27fS[VYF;y ?s%/Ԉ~7:Ef Y]~ԉ= 0f@`&9{4'y%9Ky2t`"کO)3%V  4i/ޤkO"]'3녎+i?\m:C?V,$,̍)B0c0b5}a dtb厌?4‘[͈o|Rr 9>_Vl0y&3GeKw eylYW @~ܹ_IRH8pguExavU_ 6ϯ<'d] ow  }]\9uwoϒ:9+#@0L*Ϡm( d>"O_&p2P#ƉgU*MJ2e:ˀ' ;zPdgINJ./:}:{R3  JŒܿޞ. ,<.R&"0];'k0jW/oS@/6LG|f:o귂Zu;;_W]@! c6ٳ@$Á|\Jb5贠#m}Xw$Mت13-$s*)0D sǝkۊҟ<+(ywB9=Z0T7b8{ngj@M$3$HSͰr'$eجI.=Oaf爖n eFgL}Am6_nǎf)xsl]kl~Ep$Đ$O-dǻihnx:-ר@"92؝)^)8C:fmG#oc?*[?<{LfR0{:1<=zEs+Cg#mSϣv=`dDOFx^kTDqQΔ.2/줻b<(\-`39+(V܊("EJ?ݡKqbZݽysfwK.%^?Ύwv߽wn w }'HSB~ܾvyҘX @Q$B>8=p,8,lϟ?uz={xYdݻ?~rz}̔v>+" -2eO8m0cUN8>_/:G`B M@z=anvQFPHRF:-` AG ܯ"@ )AwE  AE"Dž%p VڵkGœ_x/]oUdȐԱOzփjC?~h||H/;4l6ѵɎDBiG3/DpTe|7Bt_W{> @@H_=MhWGtP  T @ *|brPɣu#@@B mRlL8vRN   f~D0-n|UNѾsB8!~[vʛ>>n.:zVL0ƧQ"aB޽H}dt8('d;}2J7EOc{sz-~fn9@7W@)ZѢgMK !}8L@@ @q<!A'1ӁC_q<]F @. $IP.TS`*@%                2@2g               AF C@ JȘGP =                !(!cq                d @ 280 yY@%                2@2g               AF C@ JȘGP =  _ToL|A@@@@@‡OϣPBӮ;hQ(g,z$ԺU%9}OӌkŸ$I<ڥ,| 2߬WW^qJ.d0lB{Qۧ>}zz$cF,;†G_ܢkz0@Zj&My&Ǐ)@H'̜ZѬ-~PNmstvOѬi)u8-74C@9(W8z=M۸+8c"bD(>x4{Ĕ)i+k ,8$njJ>|bZm(__eZѕW\A@@@@@@L7AUûx&5n2SrHG7>}I+t^f"Tt=H?_zݾΜF n?e)1on/r.A2͛F7|CŻ< 6b ї>)C8 0x`Dz`~ݺu._LOsΣĉ׏=Y =,U Q\Bz܏z9cl8).09jPPz,e;߆Ȣ^ʛ!"gm]tgy}fJ^H _+wHڕL$߾@,4<1X8yB >"߯_|CK6;5Xc3dKO.RmjJ       GcQ=C=={֡|y3{gIf^O 8嗝n$ nѼtk~RX1jРp)ٳL?sΥ"8k׈O8 a.BE͓Gx ըQ#]=)S&9YfѪU<_FR(9&%-"}U;͜x@Å M3TC{-5_ .0C|H`y' Αi.#VlCX8J!EcX)%JPݺuq[H׮]} ~=.@_g~Y|l98MTqN\a$ׯ_ӎ;hق>9L-(mڴ=ztن'kCYiF2yH!x gIC?LdzfSK=˟1%ȔJ/y>EesftQ,QxiظN߸磛R?ŻDw?ҍGO2y?]k`"rd4A1|X.t Z(:-;HEBtD|vZ+|)>ǭY %*~\tuZ0=!bDHyӧ$Q#Spag[Q`yI'~ފw9[en&YܘL8q;oەoP$GB3vJǧ8Q#Qxq>|.w>}C)%S>8Lxȏi)m"<Mo}ӘUKש>P"f"DUhe8">Ѻg߳WCYdU&g!>[e97o۴9vU[wϫUJe'x}=?w]L<6fX[{N__zqaCy0\x>%3%K@y24 P$1ډC=}n<|J[]ո;Ww̫>&WiǏSsJ, _r-}z`ڵz|Mx q3g?)rpTH|^?x^7"@:);;m{73Z>gY}(D=:K]7v.D @@@@@@@ P @0ltT],XPccҥ["t}X0c3ԥdlc=TQ%_‘3|xʲf$XQ \( fԂ &2g(k ng SH޼[ZVq Թd)~Py,YlڥJ{jEٿ˺,)RN%&Oj/%:w$DgUUm 1i%cڳ놛^|M,^3#cjB(ȳM\=C.ؖX>@, kW^q*r*5k>.^bWUѣ6 Yo[VXJղ}s:B|TZ5B^e1Yfիm9\ ۧObѣ3bgذaw^#;qԯ_?!-q=ܹzQs5JN6֭[g;yݔ~:t鵡pf׮]T@j %ZhW0x2' X%Kfֵ/'Y=0+,:y;I&k||>8[n {Ƽ:t落*;wn)R {޽;9c4P9WV,RƢS ]4m۪bcb %L{qg8,ba+VJSї+}D]w PV*H7fyڳ&rQ^BBm&CO[DK /jb,|0[gKJ(nvc1I"_yWў fvTPxќP^s\j2e2nRsj4kji{ ^p#]ت n׷ 0K{b}AY퟿"U SHU-Cplr iچĂ3cS?WIYW *ڗU h4 jƯ-zLgP ѶyU*@\%Ǐ{Bx{U5F/ʫWg*9ڵ~"M"83јWΫ;Y-5 MŌ&8͙/%D*ꏟo Vmyߡ&E=!EGz=yq@dFq;qdoAm( NS-woߧ-| E*.OTeQygEp1@p#J0~b5dt7n !, /;uD/o~D#qV j۹su*T0իwI:>W:l>mذߨ'/C1cZ<p>cy.~ɗ0alQE.@$^ޔ PImZW2 2XL,,݈qZg]#y^>~}~{jYrڑÚS,_oӦŏ˜t^~@n@ʕ;I?+1*VUL=PbŲ0`6 P%OӦv267nGɓ=(۸i? 2_[]2_ |Ef.N*G]ck;_ׄwE ɂTl PYZ[Peqq,{ ]0[nɅZDٓN/lQb'6Bl^VT?|}*4Y<8#@ Fݺu+?}s:b*ٲeS[z^`Ȟ"}Z~Ec5ӽ=3խ[ׇHpgϞy뉯Q%y=vP/Rc=y1cǎ4{@03ʜsNZdj"5e̘13>:T^Xܡض\ -<@"DqE1 ]+)|І .c .R==JPnx@ {:ԼpM|Lj Uض"^[\7@$\"i9{, +DQc'@=H̰&U8pyˬ4썢C_ ɺ31;N^G=t,_|ƝEԖU /֣gKJ=U!LbU]ٞ&.bEd%v8|& _R_PXЭr!WKw1dh=Zىcʘ{'枸fP=j wEE[/߷]k]+s >|]eO jEua{*~l+ ڮs۰qMʫ6u('ⷖҜIޞtN)gP̈dvu+U        @ 8/jk!(ڏjL<ԴIa/m(޴pCdBDp8;E2eOix >Qeb!t.>BTZ/%31AtVFM(DlcbaOklp(y2m[ƹs7}N|8w>FYy1h 7n}T+5nTZ?_ W(֏0.wvN⼉'J P\?5G[ӽװ}{:l̓Zʨb# {.^H:u2=)Wvz%C,V~ouus<u7j֬,f]t .=ʕ+y`OgqN 䐘ˊ+R}k3eX/ OҬxnwH;%PδIeU^h"-/Va|69gtMMhyf)QnH;{fCO/Υo_}?iy%R0 D6C4;6G™]3vNR$Cu!tظ>C8"N-q4nWrk+@Y-@ɑQ}>FGT/ M+P/&?xBmkPYi+ڕI+p5"My|rHDB _{\fkWP& o,^J@սP1%CgӌM93ʜ~W )ӨhN5];Uɯ[g G:ZM,cyFrwF Pn)‹q16fec ;,Sf&@}/V_aoH?+(WSzj>=~Xx(i ї>L3p3P 4k޼,+Ov_ -Yht5]4n\*U, 8LOb4Xy,b7l4Ԫ\,Yܗb| +hH.gfĨ,i6y@VU 9pe=~rY⛤IQ_~ 1&!|nQeKT898_6vdyj_^oxNKtnU8W.BX}ꗯYG,2oB}h#Vf~Y, P>|F*^t4̟ɪWv;#E'oN5  t@^0~u¿+þfF$Id6{09/< e|<%Qy< .ϕ02kƍ4ydU,:ݻw[yT Άڰw%Cex>@>WDݺ"@iժ i}'{?QnB 1o,<=ztZ燭N:N]wr 6<,>b8fƞr޾LGDya! ߫mڴYE-"V`Y*@aQF}Vżn=EʬY-z% 6E0}tGhgo++X#w&=b7/zҧzQ"ZsUrN o0ѹo+b=Ѭow T߮Pt}XcktU]r3j?[H=UUC{:sPpXG exaĥ>qn_[Բ+3fNn絯aS7 =Yuk^#@i6as0˹gT ?^[`;pAc[2ӗ?z?N/0Z~u׼cVbb6㗯#ƞGX@LxGͳgO+4=J{hf 8qGQvQU,kJzDel(k V][]Pq߶"2erJ"m&Pݰטs,M̼LЎRNV_~X_ر)IezC4pr{; PXVmb uRBSNeO.=\pSxIԠtʟ3_0r=o£QV&O@)SZΝG>_%9YJ׮])k֬)7bo,f91G"@p.GM9Ǟn>xSLž\ظ=*m{QuvȐ!~,:19{0Olsx!~(NX@<=z0<ys~-K[G*sz"l)DPm"@_tTWsLĠ Kxa9b 36{ZL~Z /lPcFkC3x5ao X靻KtV¢yjf$`{\b\SOt(Af1o`a*@.B!x {a13k tfjxY$-e-d6^?H qv2Շr5pZx9a1cփ;Wwͫ1(?&t {#yBܴO<$CݮmV@IDAT9zB}Pb!fd Ėmwo}gњ'!E+NNP@@@@@@@o @@o׊YĎf3ۀtኼJ>$LK._/9ĉ2ֱok(і=zgeKB-[T?\T[]bvܶe-ͺ ]`Z$m,t!kێ"7na,y09rU2cFgJH.s @{raÒTvnAaDRt1UzرH3fRZrUp(#G.ОNH篬;I#% -jF.@1 *6֮]K3fPEV[]бcGE*E,a^x,ppCoJ5D$Kh-A;̌ã9f &t:/ʜpx^6{15lPuaULgĪW(Tpb+^b-LSP!jҤ,wϦMd'WFYfpj<2 PX $!Ju gh>:= s*[GJ<铫.v3Ee+lc:~bLr+pU(CE.is}߬ og(<э,VxզꞪ Qn;"֥bAʘ46a[U>۾] M O_&.~ DM2WPR[/"KDy@RQ)ACqOqk^}ֵ]Divб|ʜ"| P. A\u^̬kANb(D2hpmX)W_di'Ӻ ]uA@@@@@@\'̂Z烛jmP[ԸO1I8i^+{STgیӨ/F [WbE-XaE,H[/ЮDV^#l~hri(ǷT}ö.@~ a[;uD?,:x d.Dfa/ɓZ\ LҴiSiUPs_=̉1cP„ {m5N;;'\מ*@\vb >;ƍS… n؟kӏpUHbO=n߾M-ZPf TU^SSBw+$z}P%SƩХ[;Cp[ %މ8@r  )U1; P;R}^uQeH^x}a~=etEig2uWcQΈf={N_1+s׼UEeӄg³KFlRs{Cw-2k3 $:*٨b/~u׼1u P޼O}l0",a~"MYQO9\=V*=Ϟ.~_?Td>khƾ <(نB^lSp#P3 ڲy}r/ ZrSP#J ڌ~1߾7KY=UVxyj v/^P:ul/Lhț_(|N2e}9rÈUfE1hY#F 2'}tUa]xl<f'I$B\e+WKz ^%39q;wNiekG9_~5w P:t Ė_KX޾}K,ҽus~؆PexR l')C;r_B' ڍ.67 +& 1=%;=}A/⾲FEs$3F1W3fZq[OF/ZRm?ݻz~Y P >}b1- $6\b|pE PXƹvPCзL!ਏ8yJD\`P7g/t E&ʙ3%J[/-GW8+@iӦ(KvsQ|1k&BIlCq(ܗ2MG%6mbcN|4q/HgCQ,R˵k[]Dث鋻쥂UZ`7f<]H({Pț_(+WPա7tF%K*>Ѷgg c?Wg7e~I>}Rj԰L1+@gQWwfX(Jry|]efʕ "n^Nt ԨPEJd6>@$#'G/mc__wfر j^DWU+b}!=ap{[K819Rfֿ.@9ESLo<#o޼IZ NxysРAĢ6G n׃2ٳg-)YU[6޽*/ v0O_[-3u60m4HlJ*Xqݻw'8dɒ֡GCuez(M6}\5t gdp4u:߬)ve,94^W5ݢdʙ6w3Q)c2; (bƘ{](Nh6l=CMllĂ?[U1y{@o7<Ӕ.HgoZƎX,0(A>sjjսˁ]ƨӶX kcvӥ 4ix?F       'J,gWK{ҽ[-bµkwCRTnjղڴK'N\6'RL$Cpx_b?FN2>|߅bk,"# 1vlkJ}R~!V,Z3 Jx1i|̞fZ/i'%[%itb,Y|6"*b^C}dJȴ/ P4.MogCeQc {cqVW5"E 2^Sԭe+'?wP*W.@7&]֤ d$9%RH4eǷTcie,;V^ pC5iݺ dH,Ղb.^zEM4ׯ_}/^<GykXn\@7*$&Lh kp9Q3) iϜ9csB1J9烽N(ر#]tIت:'>:(ܔE ,V`c6Ȉ#d]sǰG|9"{UJleܙ ߇,a=K!C8c,4co'l?~ >RN-|׫W޾}+xzT\99ćr @Ey3i[/"bqMSHewG/ߒ^P>縭 ˘DO{F8ʹ 1oa% c:su;ZC[?8\K4g(3ZV%ad 7 _rjD%Y}\mtӗopxV#rD٠cXn=A Rs?s~x8NO߳W}]||NUW~ iªqǼZ`" SbKP葍l[;Wwͫ(|}[٨Xe Pc=Σ(!?7 ,4(s=[qz뿏 8v T1-J=6wN٩l&J@vc7w{c{^:v1.‹/g3{P x{qXp/ZrhJDoX^T,'Eɾe!Hw S\,oӲ$ٳWTb3{UևuPYJ1 ԩSLڴIi)E V; Ì*x5n2LneK+~תvc}tm"pClQ2y-1'UL9s7+UOxx"E~"De:ۛ7oiݺ}FVryEB%g^r ^?|2S?u ?!@Q}<޶`qMO?vbqћԮmܝ(gܲ|>t!uVy7nܠ+W鍸 W&Ζ[ĉC&L0 fʊ+~QܧL2"4eэkԨ!E j\[^\yՆ<(=m֭9Si͚5R+W.WK.3 ;(Ϟ=aS cԢOj<~'J}19D0ëWra׎q3O4,a,RH4c #T E9~)SJ *[oER xyfaÆ޽{ծ<)yj߾QnJ4ih?~LG?9sg:OU'8y@I?zJѾ.9 Gֿ?ppXXTxF> ;O]O_;w~^NzW+btš,xx`F{^N^ǘU;^Kcn>Dџ%@C|_28y†M%<3UPZṎϙ+/ HN'xqݚvF?P O(uġ<mѧ/ªOXB~y}fNcG>^\?|0=pUÔǒ>M=|Ev!K%>&,q}Ie^Y]z{aEP딐iZ*CXz)`6B nOf̨ה] YL }ռ?ҷ^T P8;U ]9Ex"?7pQ?ٻ5z#|ČlCe~Q7?qڳ_Ѡ.cӴ2dhe=Q        @ Xn=8C_w'ѣZ*L NU ѡE}cj6ݿ=}W(<Νp|}Q7J˖t)kV" gN 2+׾x:rQ(FĆEۉiI&^ 2yR{_-Gz(QnARiƍmcل+0ZMYf Mii`rwͽ4dou szf ʶ#v`O ,qd<ӧkujCTZn=Ab֏^ctE.Q !@[s,ٹzU[5PƏ/fo6n֢E ! ׍urҡg-["G}5b\K=nfɑ`C{7nQߞGկ_?˥7Տe{э#(,DgUTq(\ӽpq%VZU3>oCOc)5/m?v.)02$J@~l\,n 9y!0(G *}mb,lPX`0~i\XLmMy;ͷoƄ焅&qkvΩPDRf!q P|kz)Z=%Sė!@ w  Oq;;ȱqvJ-UjɝG+G!@%`HKBM&_Wd࠹bq|]HqujL%b_/^S]UՖڶLŊlGK(df,b2]{` '*2xP+YUsE:NÇϻB)pcfy˴q8kkŃ+'ǶƾΟ3?/ۊmgj8kf7 mۺI,H"q'3c/6sm+˼0حua+@FJQ^[C?if˖7Q5j;wimMc.^EaܦM%ʟ/A8ӦMK"yV.2e mذ>VZTT)k񂻽>JZj H yxBz`\ˈo3'cǎ%㊙%R#=ڨԩʳ92uM_}> \{ѣ;{Mk=gOm Ӻ ËG|MR[hAo߶ UXQxӊ:M5ֳgkN{BxAqźZ+=?pErү?&)C*jP`6nOc {'NXhṄC_}xuQuk=z{:cD)bxٌm-wޘ[[ݓIZ欱_%U |ʳ{I=\x7OaQh=jڲ7 MfNxi/^BckOW73QY"*}2zU{1졆͞8I?.zMIgΖ;jR,aV `ұ|J+Oܸqf,dbzp@Ht" ⚰"} #îA:XPlƖP|w](J' I`z(;pv!yR'-s9`JFxNçNYP8v4% Am_&X\|!uQ+wHGDq)ws"$Ý⭁7 1yڻ_KPd' +pP݀o:ҙr|KVkuߴŴ"2A  +bo70`1В%K O#,h֬ mxEA729 ͚j.>z ]5 >A@ ܯf7ۻ@[V1<4Jw0ПJ']@@@@@@@(AGD 2Ƴf?u=}kk\PJ 1PL46mڤvu{;4{*ׯxFu=</(5nܘ=Z3FQFdOm<|xcO/m,v:xo]@ Փ(%~JOo?7}Ϋ' ΑAŤ4nCʻ P~.<+/! :pHxP Og#Ď[C:խ[7ܢE ʟ?<m4lؐ+1qE˰\åtʻ!R N ܯ$@дE^}OEد",TDp1I|J31n7|T nH ]E w5hk׎ŋ'.]J-[rM?&~2`yf"D^zQС 0X Ԃ3__=}@@H_=QhV{>樊[@@@@@@@PA6~ׯ/w^3fLP 0;b"c;J$ &POFr4t !~͖;jR,DmQ(eXAx> )}-~fn9@7WwA@@@@ F`8qWI{<xPpG/1w#_rujZ\3j^+N%؆ _Hw>rp%JDu>j#kˈ9"}mw1_|tAI}G:N_?8_Tdnjڱ*B *Q U ُ:b ' rEE۰a?9PUδI)}xrdOL+1Ozd;JD~.u::ʾ";vLo=vN^=N@qbP̩e;Y['[PNmst?19s3)5-NGv;{zꍻ@)w|74L!:-I> d0 JPPcn4 }i&ښٳWT|7Ӳۼڌ'O:|kys{Px1e]&сް~ ڴ 6wgfDiV]~?~A7 Ź1rQ0[ :GXp)xXj=y(C'H\R ~J~С*-la~:tz &uC7)r?ڻ4m .yE)efPoBӭ7f>x!@ ȑ#5jj>|b3:]p>,D/Nͩ@p۶mq֡qV*H[j2~qN3FEsO\FcQdPki:_*yP33]-yc:M8_yMEf 䠌_8ʹ|rxVx3^HoeӔdֆÎ32*A">_Tp`JLmz9g9y>a] [y{.%u_P\~;Ӿs#ōsY׿ZcB=%w_6^vj1?>sO#8xܔ}@ ޳g; W8"(K#<|K V\ ֜Ed,s J PA}Iܹw/je?joi+VT_SԽT_} @筏{1:Px :WRdfZ,>,Gn9JiBbŊQ O:E={8{C///7o<7oP5)a@pRJQڵOhٴfM @q85 e:XP P5-O1"Y<%6^,`a%~'&vZZസſkQ26zג85 ~W}ʙVQuB>Pϱ_W=MI8#mɃ _fD!'hz*><˷a!͂3׆oz;߇~O:B+koyM!~ˏ ;? !2/(DU7!3EM4_vʒ$G3[ @AJ| rjz-FBǕmۍc.(c6lY%((eD{a@tʽ%fFEٛ7׎oKTQ o1.]'ؚ(JT)˯(W~(+,\Su ҥ x*!xLXqE Q(~)vAx5/p'5HQ2?~#вT^ʑ&lT>w&l1أY'e5SP/QBrxSͯύᅨg΂3UN3ZXDG @Oǯ$nfĿ=]4 Gս=yO\g4 ȥJ|S(J`޹tE{TS'b@Ưnw^w9=ˣ~ď*au,{@U}쀀=B J3LgW;1yR7V `ϧDηaNftF`deh^uCZȰeإnGh <LH*Ÿ=jMckDw(%"#E6uKԮeq֔3 }9u|_{M_t(_@qEjQm)[w%v+/<@xz;kwt׍ ~@ UWd*sk 2׫EK/$feɒEKG$1j <"«ڵ0%"!>ԥ@ զ@ ՅsJ? خ\裂.!]avEve8߆u:u(p(p@x)|Qr F禮t р@dU |&#Jts4lڼi."sb,߬NbThJ8.Z4HΞJC}E1#Ҥinq.P9\χX@N]5PymfWP&(C)N׍\0.x|3>E?6S-Yl9h}Jj٢"VbF7 +Y%O*UXŦh^E?fQ_K-W=jદDQwBpy'J}J7.C~$G1b&-nsm;~U0;$7Qˀ|2Rq]^;ϚՓ^Kt/ԵD 6`Aݼ¾:y";+}G z}Ezڸi?#(۱~ɓv+'.jvk} xw~ ,(#v7>h`NXѶhz4t|%K&M/-d|.7h׮ߥѣ҅ a>w|E0&Ga8Fۅ`%H@ǏӸqtmױcGQqРAhтbD 'NSr9ȕ+ժUK)f̘Eƍ˗/0*UP]怿iرno*UD -XTn]J6- oL>8?Ǖ pEM눤@IDAT oٷۂ32gŊu2Iy~:3M9)lP5jp ֭k>}zjӦv wưa f޽{Ǐ9t$KLe[nь3x 8qOD k(`SWoG%]M3~n*=}!5c)IB ƿ_IMKO\EkP =M?, F$,c^3PALi(GI 0V(N 0)ǥ诹>@קFNe3mb74c{)3MOEy!H . 7=x]3E87$exBRu-urJ!88 NC'Ͳ0 os{k֖} t}#FtFoاOi˴Yzʀl7$M(tp.NFu Dqb;M{N]=+Ǚޡ䴱)3Ҡz_g`zƿE[V\))NF[d3͓T1:5<`a|u gBa.CW젎Ɍk3HZt3k'_Ə_Pf6aӺ( ԩ]VB4bB8( ˶ 0G_LʔIhF(C97 }o0XFG|L*RN_*WܦFAPdu!yi;uNe˸JFr6ujZ[%4494yR|8;r,u>2e9_"scS7nܘʔ1o[V1wÆ V"<tՎz-O>} :*#F^)0P7n\@DΝeqTPh _p$s;'믿nn׏tKu *ŋwS*Zz,@/֫0z̑&k W59s~-}Gٳiڵj>PpCٳgp|(/ݎ9rØ^<)W1?sˀ/m\=GTk ZwoJO x 棧|^6FVyPv-|1xqF-9^x 3.+-nfX\qr}=oY2ʚ*VD{;nPi{0[$PNFW P`ܧՋ Њ Bq jQ_%&ceƺi粡f";^zJP h @RXEH Mʇu[ӗ-P@K'$b<¸Jc+~X#:yGv mJ6 kv|۰FԵra PsW's3\c<¸d&YG%fr]f~cD5.W?˗IdtvYZ>c5E<%Ku*Eތx w}#`2ed@(̘IBga'\[LυjMqw|>R&NZ ޞ`;U-G0.x`UP$x&ᲥA"QeF~x!J>R%iFM_^BօNF}ц 5iE~#TVHCWOZڵ#>YG0\tK2MvR7È i&0rq@ Ri/V `7.+<(#A#Yu dE\Q(2 mG<ŅoUz+R$5o1vE> G>|}W""Yp4*03}$Xb'z U(`y['`Y;-];ӮZ_( d7O&[5N/8%0/Mn##{F1emNXwe1 PߤZdr-9 !lF , bFV;udq &x70z2p@92 px0IX (vbR:܇{ˮ;E4foGD" ;%JsP!rm&IQ?`xWxs3:,=*௿ep䆮!CF1Wd&0mH 6n9ށ0pPd8fhь qf '*p,^YX1Zr;m@po&Y2c/o8%iRB-.VfUi߾=}'FrUh|뭷x&ŮŢ P$י-*~,#?UG0{R%/v8F]`*2J;dn|sB2:̺P&OFy)vqW+XP(eԪop \ҝ;>V]+#q_iE`T3I)MT_vf^x! }+Z>G309+nPY|~q i۸s|?!G `Pfe: }4`ql1-)5i%2@T(_HaWra߈iml(`ϐm 7f|>$z `<"zd6mfXQn/ @߸t!36֗A(o2ÄdT@X.`]:1XKSf+Nϡ6YDR+k:0SL1wbF0&0x&3ralXޭk- - M0xЇI"la@<Ƹq6 L'GfWc,O00NdY㠺6ojl? F -t<>˹L+E5{2Ox 6 Ҵx>ςGPˮ0o>Ŧ$.Qs9 P0%YWy 6y:pKAs,@Q0l5I1 ȑcǿo<`bdkW$7LQ\o%0ר*-y/F1\6ܳT677M`9os7:n{EL# :Z~p!4[Mف9sBʔ:w&o/Ԅ%̟(3x>0` 1ϟEdV+5W W cǜٟ =cIs"#\HA;Id8ꠜc(s4+{?N_|mTJ>B/Еӭ[-Dn9Ut6?cFw mzP`nڰ{)Ό+/G5 "L`5>TOwԽ{m*QeP۟ 4x(h'KfO(Qf ͚0$fig]VgaW O|`j׼`֑=IpѨQ#̙3駟yODJ6  0nprGRJEv1|#N ˁ9k _~4hӧey )>j}S0P4ώю~؎:rkd"m%۹?ck߆ھ5wpq]$ @1@-cJH`zLFp_eZ /]Ծu k%̇ڠpEz=NWRK;R: r@͘Cw˥+<=tC{gqǮz rN>k丯2b;a;ګTI@x ЦGRc>l&eEj.Bk J$~Dg[W uʘy`0Sb.,҃lo(Q|q?}U@^uh"UxQx8W((g =%xh%"?P~WASfNk}Vipæ\4SEQx@*zv_D?Hi(O{`M(Wr'tlgf$jY )ۃ :3J&/;# LpZ0NJe>QrovT!5k}B @ݍ} (* *\`(߲ɬuT oH++EovW;P @A{(Uڥ3D:uQFeMc0tiP0Vo*ՌkȽUW/D-WVcZHع.UC=@Whe{3v/iץ#|:ե=`6SyI$Pܹ]s"a3K޼yEatY2P>}jԲo o%2|p` ,Q StNA_Y@#OW2d -]BUTջ\jA>PJgԩƣF(]tݻGO?0$˸l2lFh+!M;$+wWo̙[Yf16 >/*PfWZ @`FcjZ0ۋ}pKp3吻'h$ۼ;cL0Gqb">,3 ;}^6ֹWY+WK f:_,H旅ldK%]`|ܺ Rݏ Z_í,\UPTM}vQ2Ľ9!GOSv%ЦJ&vS\kD2Վz PX7| @'UT _]g7`hL`c\`X1i) l?3pj ]S*^b .tGU|U%X] 2@["oL> l?lW8{̒y l? Ɍ5Aa( ehRl8|o?$o 0>/f3d>!5+xYf̃JtY*ImZo/Ů2,Z1y+gW @Q*#y 6[?lG{^k@1c5B^ows΁[Pm)E%7oƹ? *{LOt!fM[tj3w)u?`׵\`A([a 0H W:87pI㮅@rejn4 ~箣L=-^RX٣\IDԹsט%%Ԡa, 1ҨPev+Q(0l%/ߦn'YE?OJff"י~ʢ^lX?pzt}e@"=Z)2 ǦMREEji lJ~Y\8qw+#\azkٞHdnPmV MG'+07hP+)n_j6aT&ҩui<'3nժ"URPuXj}My T(f Pt s 8H`uWDjHfV+{"[_(@Վ>TB>hNU,UH%qnMڞ$d>iӂ_BN(%ϵ΁t , 0U@n!P  bH e;lˮ|SeP6mDӦM(4p=c&*^Zje#gϞ'O~Ydpm&j˗/t+ f,#:BXK۶mPB+"_(x>t ٰa4*h-N #F;#r۾}`3Q:t(KNc,Y"\7!A>},7I0PZp /Cuo<,̪{VF:8(`Q74`n]?n.Qwe;G$çU^ eu/^]l2?HQf^A@X2˨U;ؠUOb\cW+w!a{T-B`}R|7*` T@(TcѮ#=ޫUT kcUzl´]ǹׂmj}SV'\zpAӮ n_TQ(m.*S|3M!D LPt_f\^0JT#>\#~@` 2*N <}ajǽvՉ^nJJIMݎx?cڧ1mVb5"13*1+_Wp/%j_` .V|*/(Alf.f6k)+b_P(VsZry3"`/ŮZ:w]&@(`8xл ,2vX9/o[?lG{^r{PtClԶ 4i](Ў~8GmQq.ŋOucGOpjp(F׮.(*GxTAfՠݦtk`_2y qǎ &1d|ڶeÛ+v_K<,*e4d|yqlԨ4խS\\]LPA*,*nw 1 P V1ߴiSrS adMϳuQtLBgv*]?Oʰ-بrL:E7#RQU 7>_ :KV!qޣ:ts!qUVQ*QVJ Q{9u+V`FNʀg1v2@odz8rDkʙe4fWT ].*k̸oӓx\ @55c pdS@ o7 D::Zvj_~ P˹a>f7p*TPPoɧMFU5!KJu`'2ϲ2ʔg8}%<@;wp\*%KLa:zOD:W(;Y 3N_M*H~zfavX>z7֫ v3 _]D:S(vczdQV-Z{i|ќ9so_q^Fk׮QdT@G(LȶR "P0gWjRcՇt+wa~ʱ믨WLk[]d mUJ@Oov U?NoƍM(Ǯ+Q;{1nagG 3Qiu0k"wj39Hg<@ɓ.%a7j1wXwfں;gWj]/ 종ugmbyhvo;U7aG@]2Ivĩ!;ګ5sCd~CHc.Lpw5~nn])jpa1Eeq~RC5H{U\9}3IUȗja&`No% c5ĤD̎vՉ^*XcvmȷaG?\8++OU>^2{Wj|RTYkK%.[vFNW(%vG5_]Ugw\(^ cw.EV*M2FZvW՟:3Ko5ɖY}Jaw3lN;Vtvf5P"AJCZc.c q C ܖ},\Џ$/Hs-N8RMX*ڵ%i|meZt*e4c'Kz%aR"[8/U/P.Ä0&B>JOUP2&W2?y|1G>ߗC!/Sٸ֙i6~|Gʘ!NihРhԫg]qaCժ铏ҡ8f r!F* .=3v!KTax;ҙ#|iTs3_ȤɫhŊ۟@(b֯A`܁}m޼iE7楲e?N]ƒ @^Ce L_`5 Yx&dփ4lqP&<Ļ`̒9 1ZC&jѣ6/x c|*tߝ>Luɴ'H@4X/%0Pe%ݡ@܍7]vzyPVxi9WXaWV_Z)SЖ-[dף>eٴv@ y?z!?+)Qnݺnsn%P"EX5k[?NG @I6-c XgtQ(?'_ 97_~^KC `" @t0ګB ,VuliW:|VWP7niA-3e<bG[cU~g&p7:խ Յ^s:q473"BWb@?U0Cؗd >uWݸF@ =-@(GY!lQAN R\G>[InD"20YV5\;vge߿ PfW)LӸzTwZ1YWf2J8Gw ^+MiLW#3Pmt9= }ygutda٠ugf؄ 鼉]cc0]u'FoQo}zٹ?ck ߆xG5Z @Q%o }z~ ·k_(,Posvս >sU 'LVғ] @V(.^ }ڮz 2`&:s_4]%)"i< a%-^ ?+". @r'܈df#1. nAğ~^Cw="\C/iQ]!.zEՀ֙+6*pg)r_`ČDl0atmyn^+iղz7 ɓ'=Zk|%^Ǝ[fωڞpN JwWЦ2lǎ*KÇMz}ċ?6)+7[Dg~D"I˖Ӧfv=I* 0 ":EQ.ʔn姎\ W;P@#aL>=)f:A@.qcA׎U&StE"1a^Gv hذ!saI@ &ٰۯn/_>#ٳg˜aDxz YPKSYmWhrUر^u + 0~xf?K&3+W҂.`H,lT>8W(7oS a @Avd25`0zu="m m l(7o I<0l>u+CX*c`f[Û.(|:A*so>x,(!s0Q`!n\X`%>Ǹ5w aKU0J *HQf\ 7sK"9`&.8QwLƲf?c]"(*}:mntѯo%?*}/C>;ګ꭮ŠS(^xbzVQ(0nȡjh@i>~)=yZ20PV|;^hR`ІϬ03Ό ff]f|M&T xK=3#α1~: V5AݸK6vxv|v*0ZZOeL#|`_n/̤=l4'ަ\5 Ԑb9Q$u'[5+ }謂:Q-+@eAt6aj((vO2C>jU]jYC{EPhv׫P+Uc>{WhIPMEҹ? *Yu頳h'J/L\bhN2JK5!\ 4zR]-!ܬi9=eJ@r'S "P:uNe|(C.)^}E)+O.+VԈrT[ʖ]q X^t&"pPWk1y<(*B-s(ξYEX`V*XjUg^VG;UW- c5s_}y$M2 ͞K @LGYLЉҧw6lS7GaGK5hP+).'şzcRxP7RzFݸi?K naW dUjF| 0@yR:U慣 S|.h΁#|:0%paTПF3o z/ZAѣʼn3&͟?_c*@ӧ}N0&M*e2^zQܹEʉINdc&f͢8q\F0(-[b\F`T˫P6mdRzpV:u*-ZXUŋnj3 +?z(*0̊[u o9ޣE],gPR3ɐi*E\HXbя>g&6vayE 3h&KDM!uF M!@eqhWj| S6#Sݽ;kxի,?Go6X=:o֙ձǼIݑ`(*KE&.ԺO1T׮ QwRLtd9e+ U v5OXf؈r]'` qŞUGY}h@ ;am^֣jʖ:P"jȔmjT\uVXi wk VFۼ4曝tl(H."PnxgHh yҾEM::"aěUbG?lG{ zVgfq>eރAJR˲1]hh";OoS1NQL%T ŨEa~R0̸U53 *AXvԩ]:3ԩ] p}i/](v )KIٖ,ՕBJxP|/nQ*Wڴ$߿'?͛ `wU`P8A-t|U0EDb@Q6+Y]kRDSH7FO?Рi׮ UP@tiUx&5m6\MjĿŬ$uT6O|L[a@楂 .9wlee2;YP^j֬5A;P@*s Bg26={Zqk4`i)Mōov3Au @}^׮Fŗ]YF9Ld1f2Z&l꘦3iTY@MVէGBQñ8ܹsgt-7ʕsmw߹E~,uZp7n\ fTP0#ObI:b4bG ݻwy~@IDAT޽{a&PZhAŋ#.ѣG'Dd{6mJW̙3駟zdAd͚5x<(*Ur;(.]Ї~(y!H17(KWٳ5T^t}`^Kd @ݴ"% ٠`_ @wOb1qC^Uʒ^=<(ŘaQOne["֠ע@kaPKqY<~[UٵN:VFh<ͬ]ر0kndԪtUd" ;v Iz3Xfcj b;pvիj /0m:D:m8WwN۸vt^TpK*7jt|(Ykqj P"qT(V&Rlvlf Ү\ }*Q,`xץW̘d(Qe| z-Rń%-VEpa*YHWP~[̔lJxVY'0\h9Ϸ*㫿߆pѴv 5 \N_m\ c~H J){1ۑq1U4A:,J a#s`eKMj)bg{ev.Vψ Ѯz};a;ګ2'Mաt}s^w$-;#ގ~8,gx{}ܗr'#bp{%tm<<54P"A=)ZE`dVΖE}mPc {ztmIAeD#Фuޟ]a\w!ttaPTw=Vec`b*F?lѰT<{p;ttv,̚?ߦG?CjChl+߼l: ܈yw'7`WtŠ&Ѯz}_ݭkZ||-qk>1(vW;($@A~:m Sk!*G`(IƦ*Hov{UΕyU@Crml:ʀ4OٟؕCߝt_ wt7^*P܍o]Վza`V}ׁZK+;a;Rv_}9-^L۵菿/|Wր@$b`=KK FlnAT&:7CY|`1xCE 53X#\ qif9揸Vx ltC 'N\ƨ .UVI`v솱qf`>}S9-&og\`ۼYxyQvPrNo0a 4pᆌǍGV7oF4͙W *px@ٲ+~^~%YE@QAZ1bD睱1 |$pķЯoC5@aC[1,t'sj}%oFNV ݞc~'ӧf1@c0P >xơCghl@}JeL/ ?F52=C+mEq~6 ʰoR\pYl|PFgص-#LRx!`8 iG@8q)c*6(r,yra|[C2m(Gw[RѨ|8_kwM*Quy~/gv .$~= M.?]^|گk~ }8/] Fa}v[ +ycǎT@x`غuN$H2fH dFhW08A +W 8?,)RZ; 3`nrEY#:G2K_ZR|ԪUKxY$P ܀`ٱcxvɒ%yl ۼߑ ^qy]Y[o1]%vEO>B`;(xK9-sblӭ[7E(PRHa , *$w(qD|l&M<Ro  )`@7Wd6aA>UI A5&ۏ~7L4WOf0s! ,b#jyǮN^㧦:>qA?oy ݨ0*`0mD֐| 9=v"vgݨF&,[idxP1 ^]Bq\Ò¿wؐ.\ȉRTVm,=u˱"wn¬_= @άS"00=xN_CFo%K$M@'ǎHhUàN \5=/Oڔ!OĊcW(e"o2oBY?[jTݽ~K ^}eʟ15PAǎjGT2_܇zNSl;QkSqZ*13c00~6<_ŏ#E2ܮю~؎:n8-}9p`/1Al0Ќ7ԼVyfJX@K&A:2Xw\9AssU|)I@Sln@3 iD^[/%,)S9F.-zHBJ ޿*k[5 huU'k#mq`r֒}5|hvԫP 0F f˚:ɌҎ~؎ԅǏ:}Z~a+M?일J$\?ܜwdZb%%,l؛]e]{ ؔi3lnT= ]BDxP@! bTE>θn Ҧfō8d0F_DУgto^>f[̕΃SKw6C MʅfS#M٩bPv$05`80vJF2\?^z7  p/oix4ގ ^ur Nal%vF4 EإB3 ܎lR']r|&FVeZa+ӨIx0e2^7âG@㫪;^q_|G7oj up0@jR<_^ޏ?j$}_v>[?lX ̪\2<1:<@ ;ԼR=nG'2V@Uf \໾ph |=fQ_vM+Դf0z@ ^%%ߝg~MŸ_R0h3* ;(ˎ~^Wb\skW?l|8 ,:2%r};oGlON.Q^%U1\pe!68#拗,ݎIy1EF2B֬٣`){2e݋ 2oF;7tL\v1jҸȆx2i%3Q\0͛ (`E^uf y3>f{~aWI_bB]Qbd8[K 4k.NSV|?r ӢEy;vL7=ZK|ojZf?zw VQ6`%gt͓&rsAeuU}-YOW`T߬0|BcΝk,V (ǯmn]k{L:w`A?%Jv1-.ʌ&S6#ٜ$hs_ZVU͛oTӸos˾cjL-,gCR#;#e3; mAMy|b={z#E/[ŹmQLu w'BvA7/^d՟ 0v+ s ˶XS4v|0U5I4=KcYh@E΋Ҹ}-ZEQL&iҤ1ܫȴӦMMܩhe0!l0>o)Uj` @)aر}ucFNpm5d]7)cvMGN V#\?*_zP32, k6U X[qjH~`B꒠WX Kԟ*!X2tՃ^WL)zӓ ;s4=cי_zb-nB53;'uQ( f%>iR @~, xc :fo{y`x72.̎]A# eIk  }96V +}W#3\̕c,=d=pB0y;6S'V4r_;FaG? DâSRn/v!c B̒ m<|T䡒9Ӌ8jѦ|h&I|,^|,_|@tוad$`d&GPPEYRD̎Z jX0ךr0t@c+Wˠ[ iZR:M{h֏]J&. 1 db.30`,5zw>4fg^;'MY=uTl'Avv̇= `@7ޢe&O=A} 0'J$u(P ح}l V0Q%Q8TwGwypΝ=v:༩Li$Y@?E0P+vԫό(^W/$JOϫ_beM>?iS0'1]%?1"0ʙ3fY aNZ,?}Qz f߾t- BYr#zp3&=oFјwuٹ('>4"i8@N]4R=i)}4+a9sW%v?x wWK 2i$A (2 @ɓ',pq V.U.#2e-3?E]={֯* +V,.^ x((2E[ɚ5pPիWya>\g֭+/ I!»>y +O_"fA`\dFcP${䯿e ;ta"À7/V S1y1+=_o2 & ,0~30. :ء*`5rlzƿ+h]>˟P'/q Пv?_=U% {^[ OX+k|0Ƕl(}W>x37w0ʂĮѮ~:#A`GٿT1VePI8co)y|Cijd?^ |&)|;I*K^WY:Ep'&1b̀'>·}OQdEpumzрjGȗ/p}=F@Eu=:T6smyGh Y4dK;iQd󽺻OR}IYl;cy>"3{ +GQG**,wcQo0f-&;:q@@difMwыiZLg| W: QPԶm[*Tq۔)Sh˖-ƵsgN}5n۠A}P[NE: l?`q$j4ۯ }"+U]˘{b(q4hh ׈@}v v5^NIЗ_C^⌯Y'RzT!_~S02E}?p+rk>B5MY^yGVQF$JDǏ2$(o@O|iS/FyGPp4h`S*1S+JtFCn.cp9pӾ_p.3-5NFʶiȐ9:t$QDpyѨQHڵ "aLj֬=|;;5#GӧQĉ'#xs.hW}>psmz}(%)g~; O퇌kр篁^# }t["[qcS) n pmppg| W:;rԪ*exOI'y*ɛ-9̓2xf:;G 8xq 5"P|}Γ#JD , P dΜƎiV, nu\ @vר}q/:h jK.o˗Ӂ"DrWT/~@<c6x v1+# x D#ګ4eL1U/L΃#J0 8p4_\# 8p4hрG8pf^g Ѐ^bh>Cu9p4y4]:pګ߀Su|4P8p4hрG 8p4hрG 8p4hрG 8p4hрGJZ 8p4hрg,X8~Q;g?[-O7݊ "*" 3]ݛ\Nwv^"@ D"@ D"@ D"5H5(D"@ D"@ D"@ D"@HgN"@ D"@ D"@ D"@ QC(QÙB D"@ D"@ D"@ D8K(qvjĈ D"@ D"@ D"@ D"5H5(D"@ D"@ D"@ D"@HgN"@ D"@ D"@ D"@ QC(QÙB D"@ D"@ D"@ D8K(qvjĈ D"@ D"@ D"@ D"5H5(D"@ D"@ D"@ D"@HgN"@ D"xm&IjH%{{3 D ijI#~er{H D"@ $@"#0($w@:]"@@%|H[} |w^o \Ϫ=V-G Sv} $k kCzrF+v Dml=ie~o=BZv\/v&TN D"@H'Iϟm+^|V:`Q 5ގ0}3JBxNipluꔁvmk8t ֯?Z[2'w$IAPP2 [gΫN1(#04>kbh(DEҥwww~j=Mbvl䝦r/HSЃ<π.:0yj Prթ d|n !bzf|$k9oXg^]!9&T7w꫞^ryȓ1W k,5  P6_6(ʼnmgw|LiR@?Pt߰USE !~D$OfRmiai# |oc D"@ D@(qaM8: ؊|ueBYL0yRW>/д( ƌhV=gûwl<j*=|2̘ax{hܸ8.^#GE8``cck~Pǎ΁x߿z61͝3p@A&6tLFn_>0>Uok h!}vH¹k~;K/&Pi(9'C;5[ V5]T^sl{]hGT"@ D"%MVDJmzիjg}&= V. {@y,۷mi;ud. 6I:u͍wvZpppCԩ'&KD[aÆСC ֭$@.UQo \jKF]tPj/w ?׃A\8)Jk ~g6B2)q]oo SboǨ\4:pJ`A7&f f&K-6 Pz7Rzs-לĶuLFrSXqTMZbr }<+>9H,6N8 ,k{% 8h'T"@ D"%M%697f;Mo6ԄB' K+aqArlH~\֪=V3{;-qݾb~ؼ}Gt^->N9 rּ@Oxh diZa#FHE ޞ+'9ĵ={N >~äIx^~ }QeF .P. vɒ>e|iD"^2&3!uy=djkGzou/  I`YN) Դ3m-Ym *(;lF>7dq]K7yoSrgF;[Wd7XTS?p/C-y :rT-m-iS].@(N^iK  }BN~ܯ^7JgaQN9քA QȈ D"@ q P FKGh&|/ P,SDD''X@pͬEWl*h) y(߾}֭5ogʔ ,mڴ$Iȧ{y={OE(kr\C*ִ >(ɓ$E= ph-*X1G C331gpfZn@H`ypD @dPlSe}|{/n~@SCxv ^gbKF D"@H;QE PyDVD Py/ o37Z kҮ(lbPA,hOI Ϋ>t~a .̳b E{$BkK5_Q9ژ&@Z,T.#e\X Fϒ YHm$@A{J,Q>(K(v6v;nPh)D"@ DN(1|Ԇ/8%ΕҤIvvI_~Ǐ_e{VuRҧ<ׯ8ҥH}ݮ]-T(_0$Qp79~)K+4ib5=cIx(#__8t={ Fh߾/V[ܺ s,<{~ Er^ҤIaxweRbٲepi7n1tN= gΜ2eJ Аx)[o0a+O^~Y ѼatMpG/ݻwUcǎ/_I}߾}!W.͂͛7aʕRQ٢G2eX} @ Ɔ˗/>7jժܹN> ۷Ur|1tgqŅ}ĉlK.Et^}6m*W SUhh(|_OB_؛|򁻻;իeڵ+n-ϱcE2J3&@xoܸQnښ8q+F~4{J*ЪU+~Kc8|V?5j|,Çj*1KN\IHL6M2rH J5IKϞ=!G߿ND)AM!M`}ocoϯ!].A(c,Z>&șE>>| ^[.E7Ta!s.^3KCP$L #+zn>}jfH_\#*`;חnly?y P&<[i߼.8I3BXIZ"_]cHx$d'^MP>I}P #}Wv=}z38})`Ys^~b^h܇Vcʤ9w9~ZT( Ջv~t:a磴캬P0ˑ 瀝oGlؽ]e3}kܯ֘WͫC|_s tf fuIC 'n{)~~tQ S:>>#gݗoW=)T rv [|Ï bnx--C D"@ P,MP1lhya]XD bQk@>4?ʕ/?#*A3&~D զeǟ4 5r"[yV1 K4>ZU%#,\0ݫvm%4q_&L+o1WsP^=CUtPs˕+{fiBx. mƅbC頠 . OHg bRx8p>ԩS0*ǏrЏbG:h ([,oB\DK.ZEݺu5kj;EE{y:*u(Q".@>8(+njոH_xw(km_'O1V%3zh(Z(F D F.F4"͟?_̒(B%K V^-?bXb<lR,iI$,(T`wޅI&11`ѕ]% C,wwfWH@ x%fik Niy3tWH6Vi3pkioˏp5 Zl2[PyB"3 r[1EC"Gmt3] IrǥРfpZPgbF}ՒbgDܙa_y} %Spzy.pkCy ?5*w~Pr(-; Ԭ#[@FoAP J [lr\ԹFi4ԝ'@[sRys}* R0Q!es2q(-jA!a֣Z"r:ZQ4/$S\Yt_.ސZ*@A5B_߾?Ä͇PjʘMv/9bPg؇FSVA, aͫIz(|(| Z5Uup&f7(RZDRbRck@IDATR3G0rg@!QnPL$뵘cDcf+z@{1DܵxWGaǽ("@ D"%bժpظ~,fomwGTbaK%(Rqۆ*YyҾ6 Ԩ0zx— eM/Ϙ}Yjݶu<{C\F"`?8@FYƌiYY2. e?i;Q!Jze YKRZq%kbA*5DL{f^4%M[xC7[?CToqP'Snh=`a} =DC'6Q[pgos%<7(Յe;84{9<'%-PdYo ~]Oj/ fG$_P4S(9zxӗ˚Wo"/9{Nˇa^'Be6_շ,@<~kYQ此mpa*&RfE: 5^oL +}ZH g.B Tvh-UBnz@ޱd^^LrgH+33.^Cta=|瑇<4HmL<ļd߅S:$Bŭf[) 6wE251NַdiB^_t3ͦL#$y1E2YUK2\pbC/0AmVO(EED/eFex߅a,ז%-edGv[t.x][g(YEN0(k/}J7&!0th֜W߈Pn=\P%^_>!h%R#^xmI^lPc =\@5(b? r0PI27&DosXj#<[(cķs<5C qjVjy{ ƝmjSG D"@uHK qo nZj篿*@q&ׯ(`=g" [;A$d9kǔ]7~?3+W a?5k8{3YӂKupsռBU/b^Jϴݘk}{hn"fɦOҤI%pXw(ƧfG*C6mFડ*l`ETP5'`""%cGB1JU[F >[b 6Q|t}grgokW9)똺o<'M^' f^^b)-Zx-c̓ٽZ9k0x㙲aR:uī7#F,AC:z;թS' naUaP\]]YXoZ><==B=zϟ?fTS P0 "$ RAA$سgv>E*GA0Bd XPyRCˠ vHc@o3(BG0Hk,Y`Μ9ro'O" )0<C I4|> "LP, \ϟ?gϖ0U nHֱcG쳴+oE<I.}Mz`sΘQF R3{ }=jx'I1%zS{=H!wCRicη/p8& =h^|1,6t2%,r2 x0QZcy1l!Ew+wmhJʘ+$uZ4׼azv)ÒŃg>ХgJlg vî]*`aZt=5z\L`AMҚqep$3&P Pvsnc!53W<֘(.l1{Y1Eru=csKh~BVwB81+ʕ1J|c1"'ar vRO4T0|0vz@=ʨv2Zb9s Q|~E9J!>j1EeUU,uvšy:cz(^Xu}LىP*pa#bi{;A! z@ѣR(y( =YFnժDd Q kaaÆAɒ%yWE =ˤLرa%j*s͖-[`:ݣ FRT! Pu-ʯ l*@#ɐ+oܸqC.77a1c̙ĉpBCן` d˖9r䈖F-Zij y[ĄttPrխ5 'аEsKXSzN f06 P{~סr?R"UlPi=?Q*ys@q}eb‰r"5J{1B@]|0aJ pSdp B'DfF) uڬ8gg0gy:otCJ-Fmi z4ۘ*@eP8_k-(pg*`PUG/M°43ג9(8W8gJs4(]g󰃕e^jVшdУzg C%["?G#5ayT" Ʒ  m=,4Q OR'M߾NSGm$jydV)@AK/YW¹`с*AmCH^)6|*ۑP8h3iL9w.9nf- "@ D" %LvOaaNVaE{ŕ1cg~t-{#㠶G@B0ިQyӻo=hZ$ PVKETmT P5'x1AR@6?/\ =͛fMGL9 =! P:u2ضuOjߙyW7kRqns?= .-T3kׂƫ<@`jZ"Tʐ!"-vHewBi?nE28M6acۄ2(зo_ iQdpAXR]^>0 b%uV&L͹0j&X$(899B/_0-LTR4sLx4 y+ P<ML+@X|(\~GwzF4>I|Ci|6k~b^L(#o&EO(H%7k Po(aξ@o_8>xΡS 5!?GrlUb]ZO/_;r(?MU(=]O ʃlY7Bf$U}XӵLհ4b*@)W0L1*$;ԫ?D<-9ݠA9׷97&@)4Qȝ$beΝYb:.G*kPLb:`& PpfuHݫV(@1.EDj⎣G*}h 3̝ׄ ݑy/iWk;|@7h #YT PD"xl} Qի4T P cǎ"E4`5kV>%GipC}V*һٳGoyL*. 2Z m\ S_ e ~ǘGYSrr xU -MYӣ!JM gʼ;parQ PʗejeTvt5߿B€ۨY  h݄X|xK(0"[/ zXuth>yDkW)b_{ w6^z|4 gǿ lA"}=|`^2qF /RzsB EEi]bb/L JrBE ] ep5CS "6ּFeR c a_]?!7kܯ֞W>P (!,Yg0fح(@Q D%V5] S*7+mXgE%KL\Z ű84g=Ŕ&D"@ D)$@%)zKT(e˖QcO A#c5>|8(Qݼy&N#4jJy=٨ N'|F)3Q%@޽;ԨQZR*{2kxD8M6;Iތ͑x+c)@%Oi %ud>3vU: >RT5ʃsg^prWBba= F`@W!&d糹{@Ą9*fb">Oɕ-`JB 6×yNH1ZXڬ8gMbL "C4K i6XP@PlVkӵ1Mcs.?xʠ&UЏ\k['̖= "3CYDGg È!ּFE\w.<|s(C Rb5UDdcEܼ -Ti9bE:GgobNƼEVmo UƍG.Jkm 3f/tKJ<- }e{3V%`5M7BR[W{}]5bǀ暣#j "DC=;bC;4 ]to !|2,YR~vm߾]˫VFC۫Em޽{0fÎjF:O777 7SlYoD .\f3Db ziɼmXjR&Alذa)׋*(}c1A;j(ȓ'oߞCa!5[hȋZ}S^S>Y*i>'Cio{ßѢ`-إt9)-(i 5A- vڴi%#Ge֭'I*@\ՁQ5& &0!xp jo0C2c^xpO9sIMFe2Q=`ʔ:c88S cʵ2P.k?3/Ni(C\wwwCU0M ̙ΝʋH_B۶m#t1 " S c@HWK4jH^X7qÇFX=vW6Q -[h˗,,k֬X,-ҭ[7Y&nZSJ$JJ$]E.̒y=1t9N sv|'aX?sȐAl'8?axV½tC iVfB")-[T0C=9ۘe_^s] a s(( Bqr<;(p>lxl8+\է':(>5wɛ|$J |ĖT"@ D"%YLia! ̍a|v4lXiwv΋уzԢZ"zsŰ[n=?h ԩy3MPFT\[\ɧȘ1#̛7W $_zt Pp/  P#7nFyٳgaܹj vO@)|_us</_t-cšٳ֭['-^ҤI 3RJt=]Hs&H GCpX’yW+ $z7k E*ׯ_C>ڋ)@A^w Oŋ\!g(1E%K&Q7rƐ.l3-GྗD뗇r5 >~S|*-em?wK+1·A0y<60G5 l֕ q=~mn&L:0A V4&@x\{O.,94fw2c}=xHk1AZУ)b6^s/(9) JkpꅲT2?3: _B1!ּF%Oƴ0um~j!~qޱRyָ_184k PЋSegj)CZYC"oF-1(5'02xE"@ D" %MވZ߲7z\ iӦ"sK- ^ [< )-2ܿr!6df?X˗YpAׯ_RVd${: E5\ڵG:b l((DACݻ's0i?k̉(@~t2d8NjUUpc޳pn9_C㒜0D,1MOf!2C EH5c(a`~.}-^sۃ`yxZY h8{{(+KG0a%ăM7axh_}l"_H!_paĉ~jEӯ_?P<ñcǸԩSCra<6&PX^z/DDbooUVlٲy8͛7k(G_('P$">Fb$"OKܹXb|l|[j8p@B+] vCRG[\`Ș.‡;H 2r%!>]8?sYh7yV. w‡GR0ezG.W&""@I++$ #[_(:{(zk/\ 4ߍ2( ܔR7.yRd2v)a"iG>;<'>oO(i:M\cZ AOtd}[3U%;}_5&w\ ".T,ǯO!5>:g;c\jAL18qs.\XP;EDE+;gv4) gԀ?@`p` 2J\ \~{/(' NDE?𜳳]*IJC[c vR#:[p/0=mid)RfJe/ {.R^b⋟rUr\P:k i/ߣBΠFiՋ54^¼gUS$9]yy2cZr~p:k-KڔzPGB;O17\WYx[< gP?+$5$&$/5*^x\#omz\[3;7ߏ!X$%^W<_q/C Y  PqZc^I%@ a ɞޑxys@NSS/6@(¼B_T PaއKblۻL?]xpYcM"@ D" %M.F/`=$G=3/ `r k( bٚh%?=.\F/e :e>MIxEٮp0bb9 ??1'`B<2Ǒ~2G5U%'٢7T,eyxZ*@u0k{MO(Z47)ؼP+hU5ob8ăMC譎ep_)ә첾&VDžxNHK unL"zP 9;99q/=Kaĉk.Ec.s*L(x/Kv3g?V""@AzȐz2ۚ;J*B66?.\>fI5(r* 3e68JB`*GןB=?Ś%D-\PG%cb\h,S7&@{Mm)@Q3?B421=C. ڋRlcʔZEt:W85r||mYCV,Ue/1 Pꎁ3xg|-ZV9gI >g̵LhWp/MG@§(NHۅ/`>Dlҟ-jPj,̼Wt]V*ڸWD&z.$pa lL Kz6vB/1at*)2F?%*DÚW`11EmT(_Tu) KdA@Wb9ϒyDE֑08gK<լ!@jjʝ+()Sr[~Jöw&ʄZP:k,{u]&oV~ب$^tXÏ2ZN:m6m`c~~6[BPR%"@ D"@@l%@X8s#?)S7Ar > +\&%ҤI[h_GHEZ[\h0%o{K=ez5mUhy~JӦv]~{7QРVl crc^rb0zTAjT`^͡Ar( 8K)uQT2a+d`?I^0FtN$@ d((G-{(1L.BԫWmJ*U ##:uJC61N b oHbW֘W'[Pf`ۣu a$TCoC&1/ d! ^'O]go{4z]wQ[١mRXdij~mSVG%"G|WebVQ!YHH!GնN:[3KĀ ɛqa-[2S5!qZ'>2MJ\ٺWfhsXWnҸMjrkDU^Gin߼b¸%ÀƕuD[>yMf0zӰdOZ 'x+Hzc()ێ)bxHVP# |=['O=p+ްyAV> "@ D"Hȭw@prɓ'O;nOi6=[z&K_{[[==ι udn+{oZcN-?rC&1|bBg|5~x szЦgn24.:kW\μ8eR;kTRzXf `2 Qpa!&Ν;FdDxE.2,[Q {.$I6-?s}E#z(X V'\bS ua0~,$;^zo(AKyהc}Q",9 i:5P[Vx8Mh$%o$aBӤ!sP #Cf9 $z)?]/ 1fb{0|}?>߉c]U޶J \4\FdpI;C\@XX7yP!4hx .Ɗ(D$e0 KC0,Pl5 'CZʄ{5?Pb$di߀|^n5tN#@IDATۥc0 0 /1>.2/xggwxyr55"j((Rh083N=rΣ&<ñ5aD5G3Euڷz Xz&qrKlSU6v z?Yd#$N> n;_/ʉ D"@  PbјXd=hC3GAR ׏eO(Y1cCT (a2ѫ 62M&@1U!@tkj P<π.jM a I2s(P 0qv3 CLWc0*SkCzrF' \$]E~C~Q; eeV#KBޢZjDDrD"! V3DGU<ݩA"&:S%g Emûu]yτ_ BD"@ DG(ohD`H" ѳ{YxK\0a+\ZE9yȒ"+~չ(`x*UC\d =zTާyseʐ,f~mόZN}QMe(S7|}}['3$5^ b@z {T[և<_?|C1a2 8 dN`(?2BiBQjkjSʤйFi^=!0N~A%;\"@@\_cD%H<_pYhѭ47p&ÐA6TE,BJ?0PS 2P?S"@ D"@@l%@:s4n"@43zV0 zʗ fq= ')c< `h@ԩSsL`b`ԨQ.\'N#;A&L}4x`؝Aw>y[j%M,|s>%>E=A '(;g 1%Κ2>Mr{Tr"(T;#x*Ğy\{O\viK D"@ 1 Pb؄p H@7;1NNNʎ;e 9B AǎI4D%m:Vt{-:ϝ$T %Jc,^Ξx :'D \d)_R Y\g-ZB xsM'd@١\1 3'6kSP5ǯ7BF@ mB߽)Ϙ1< liv/s:Hf ޱǽG/m|t`"@ D"@0 PLD N ^<D@ DXg*I 2TIjI&gJ="@ D"@" P"0O D"@ D"@ D"@ D8N(q| D"@ D"@ D"@ D"Hل"@ D"@ D"@ D"@ Dq$@LG D"@ D"@ D"@ D&@&L"@ D"@ D"@ D"@ D  J`:="@ D"@ D"@ D"@ DD6D6a"@ D"@ D"@ D"@ q P"@ D"@ D"@ D"@ D % SD"@ D"@ D"@ D"@H'N"@ D"@ D"@ D"@ M(M'D"@ D"@ D"@ D"@@'@8>tzD"@ D"@ D"@ D"@l$@l? D"@ D"@ D"@ D"@8 #D"@ D"@ D"@ D"@@d Jd D"@ D󀓢f  ((*R(MzwKtޤJ/T:VJ JE<               i4aO$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@q(q|yy$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$(@i¬HHHHHHHHHHHHHHH8 PHHHHHHHHHHHHHHHH PӄY? q @L% ~               (@#               &@JLf$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$ PG$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@1M&IHHHHHHHHHHHHHHH %0/HHHHHHHHHHHHHHHb(1M @'@J`^ 4 Pb0'               8N8><               i4aO$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@q(q|yy$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$(@i¬HHHHHHHHHHHHHHH8 PHHHHHHHHHHHHHHHH PӄY? q @L% ~               (@#               &@JLf$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$ PG$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@1M&IHHHHHHHHHHHHHHH %0/HHHHHHHHHHHHHHHb(1M @'@J`^ 4 Pb0'               8N8><               i4aO$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@q(q|yy$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$(@i¬HHHHHHHHHHHHHHH8 PHHHHHHHHHHHHHHHH PӄY? D@ݤP"rRoAMXO#     /L2S;l84LCb\sK^ػL>>>!kxiѢ={ȬY%<3ɔNKtNKs3o^PظTQI>z/fS/mwz/?#oՓ 8|!K[}s“y$m$z'e1Dm幬CV*<     (@C7OߗK/n|m\I]b/:l\P{| /KÆĠO '-7ѽ9~ԫ/"3[{6~]EnY.ԙ+?'jIIxw;8+_|,Y%a ,Z_3đ#GN-ANltnٲxY…_81[JqϪ!0:q}XY'/EÏ#ˏd4^T_.y񿈂Lq@ѢEIg*saׯ_SNoGd߾}c%(lmgϞ3gHfniwrc^+)y`Vky4) zi X޾Ļ=rq#SK:eU9t,^/rrLc뉍K|7'm'~Q1'M bdq+/ٟв$RFNȠyc^ SˇL҇~mtZΫHHHHHH@%BҢE%)X C{oeͤPGu>\,}-}4b QKit` ފ1(yf (@qd;I"oƝҽDŽ B[klǥKޕ ;];voُ(pϞ g DT\LJf^aP^Ȗ-}WwZ|PKU3]~RlYiܸ>wΝbr;s8uTI8_]yQR;u\+V(w6m,[v)@ wh=W(Uko!?^ P&r χ:#dٷC2lh )PhGn"{A;*W\W^-&9r?&N^}ށlבתxG M#?S{ܦOJRLl%]}hrA &(ez\-ʜ9L_$6e_t&*aWS[iYzk &K|0˕*5T >[-o./ 0֭[ł3fP! ח֨Q#eNWV GJ׮]?<r~骏?.Zfd1)@ڶܫDוn)q-IR d\|[~\k٤y9'ɰEQ". P>]+(- Sx1ϡGq0T⌨+Jf,`SyNY&H <1!@ɑQXjnSJz#    % >(Orȩ ۓK×@L PnUSZQ*ɭkJ|Ba>ƮћPkIg|^O&r',I &(hhR,kOo>3IHHHH(@CO ()@MjLɓJLiϻI˖͡l n.?Zw}1{i"܊A Pbnp~GRoCrO7ӥK%/ WүTvײe)Nvt) K/;ǐ@0 =4o^I)<\ǿO myou~رCDףBѢ/ X$Jsq9b]\ī^yQ$IIIҤ ɩSe/e@&I>^Ge\zTyN*5{|Viڤ)O0fGȈ*#v $@ѣx.02B9_u+9?uupG3pM?|9.kSKR"sp?{TuV)Y [然6mY"אKl^S*;]yҤlahV>>zst:'a;ݞV"oѢ/ea'ghTbL/,c涵E\hy(iu;~w5~ŋ^y)I$aq?~z~&?e tNf|XRHX|ڵkznL[}eM^1 0رc:YBoKz%/0[nB>w;裏̡XGEL8Q}L-*T|P=k;|1Bm~tzw_Hz^^;|n0aڵ˜l *$5kTϿ$a„:|O8!~/^quYJ)Q?>{^X {3gNiѢn۶m2giҤ^0\ڵk\",3g:-wyǮIcni9K5<k? g9 <+weܽ{L4r6r YA9]~mxuԗD_7o.ٲes~u9{XBV\ؔJJEn»ZߠM}ߡwSR3H}nryd|\y9I{Xe>s^ 9L\K߯>~x0MD>;;AVn%w+Io*LIZI$C6ש?>)`۩^ Tgt(jK)}H}=2z]}`|ޟ@^/+<,+_q `|(y8}j=?#UȢY:% Id2qMyVL)`_wÊo~cޯvC5_;v".[(<+>mں\ErKִ{W>ݬ5|RK9Cjكs6n7~iL P/}&n鶦]IHHHHH@ v9S:CEY8L P6yժ%ݕ\,O}s?*b\M$~߂Wׯ-LM~;k޽NO,Clg!gCJއ Jojڴ*U/G,YbgttT A-!!4}eʾZ$C)JTfʘ;7=zH.Wy批?!YlxE7A4j(;IC%MT&O~ +D 4`իWG j۶s=~O?$SӰr+JʤV5%A,0Gj%z#p04'A`i9Lm[K/»]-6` d窍Ox1!dZnd^r-HUlDUJ'Hg :Z Yo3xyb'\cLYůP]qd(^Is# '~ƯJZBxսE{#T55scþjrI5{=Vi3@{=pW ϋq+ZVעDؠyLnܓ@fV;Jy,܊yHHHHH NΑ#Z7+:ś<`*g^.]j7M9,<:tJؗ%sz?!?mTfT?ԧtx`o~Ǜ t΅ l5\-`v|5szc 3B`l)Bm @cso~u P}sD=^|Üΐ!uc PLe):b 4nm[+`38-@3̙TEyBq P9wўW?Hޓtϔ)+7+.7?ISt:`(U+:LfYD +xFx@6-bqJwJYG/6+non(XN8.rz@ygbN/,n|s;cA`x Sm իX`.(c ya  ۷oG1; c={v^x+9u;T}L)Xԯ_?'[b*>}ZySpo1pƂK'_jUS\tI'<-@u2D{4@wOGPP޽ń\Ŝ Ⓨ;:Y^xUܟ)S3cRҩ ΢"gDFb8a:ibIg-2PCھ<$Nd BGw o~6'B|#Q9yV)M Iza'pZ} x%-X H^H" C E/|\ hOX+1/1ίb{@Q _qqP9.wMTQy]y1O4ΞWw9Ḻ>W˞cg̮5'C% b:wQ}wLR[/4@"h\"@z+Z8= -ZǛ7^0ڨڣTO*)**(va[(ծ7:Ϥ xٱ PҩL <ܴU",ۆ+A2>><^O<%PLY|AlC1_C1N( ̕=J3bF@g PLy>,M>mNWޫ^ sLe * $I0nr$@$@$@$@$@M7gtԢ|giӊZ5ߏbn HO^o3 MVn(Ŋ W^C8 y$"+v\YiѼ t{",&7,6oG˾,[00e2[yo Ɔi->@ѥjjsj\#n6}L9}\Ox0_]'dQ/D!j`^dq Чdٰ> /iKq(MvƍKG3Ict11x\1\ux#]ʼ\3v9;̎Y7][7{9-?ۡ Aj~h| K„\2smܸS5[b @ѺH%j`Tǵwu|.y(ail;}J0th3RgusR;Ct竩[[}9i 8)vAӥs9\OTK oYf |sA PWF8ڭ:9+#^Jd$|I>FZg5BTxmTPxٳ|(J$Toh/]2hA}.vyγ !L믿͢[nXx0,3BVlY}&}ܤըQ#% g[[hy޵kWٷo^#8x@[-@A8"T 0ŋk^c Pq_D,y`LB x0}zL(фA'xq͔)Sg}E&#* 6@>N:9"% 07ȰaÜ*@ᆱ (%lmɄp10.!*3+pb6d>aP#jQB”޾ցgQd(^0uZ4VE}u /rQsx0cUuD";*rAʙEZV(~b9Eg Ң3zPǝU0Ŕ`a༵Jsdiӡ-|X iȒ? 2qC;TO{<4/WL(∨u#ЄV5OH'/s9y3?[1ϐ[={,ĉLDY D7X7~36F *< 3Wa:+12Tx"ly@ O5s{uA D^03`2ƚL?]RsW߅qE'nEʻmWOQM(`\[xf;dknJTϷ^3W9}ȬľSfpn -z~5篪GAHy3:s,RT|W)AReĉbm 1Ňn.i;2L 4⋅[׺*t]v/h`lҴ+z_y$kGs ܇-qXȎ_n^(am4^P{yMh-1y`oZ;L)+dƌ5~eV|grPOkbj۲ck^fqx%`!s˻JpF / ?"'Poa-N5z)xihz_W2aapOEdoϦ!k} |yMTRt;-@Eqx^ _+ IJP>UH|ÜK}OTöw& u;Ogs8`7Vgh( JHE/~\, /} PBxb9hT$a8[-@ OF lذA G"v[1[nfYfUġNU^BJDe\!Vʕ+[`F8&/(b[͖-:}N P?t΃q PΝ;'uUfϞhѢ&cm{3 %TfMǻKx "3Ljr:/Hoa!zRN,$cA9ً P˜:{DVҵZIɗŷ*5ӆV"޵JX5wӗ9V49GU <' | Ÿꋎ?6'Td޹"LP-y?%*U* ȏye+( /=p3~摲ژj ܡ(@n^Ź YGӇaA`^5Y $뗑zu T C͖+7,:}1Q6p Y,|FQE]qf^:-(]G{AqP W^ocyhy"=c<-gԳ-67-'t[q(X' B1E[mYI!~!*5kDR{pq۹j=B !t^|>n;Oc15_Mf01Kһ){Se`c֭"͚*U:j:` PDA8o挞JRW)UjWfIx]'bD83eJ0,OAüY~?'T-@GMU2eJ͡q ހ͞[쒮n.rN #l7a< -@qgegh( JJΟ?_!/*:i$s͜9sd޼ya u[Ta Pu ( T+@A`` lV\iv[ͷ~l"2=x`ae̘1~M` adɢYfƮ(mڴ2vX5e{à=$ V%EE]ɭB~/ǃͪCg ̈́EN80HEgXPn)݄,4J##@yScn}< VK iںjnb1zQmع=mb?|xq QA0BH c7A4anj~pL~P-* cƴo: Z>XNu>kr1)'4tςV.Ta8%1TwD5t( [OA7vz5lx'8d<<_H^.[oFW#cRҲh)ČʢnpK$@$@$@$@$@(wlz]iGD8!Ed-4:?2b$^u[码M"H.w3 ڇ } Vt;3Ec|\f_~ 2~d$yzQ' S~NW[57^-@yd0˖-*[UZiZbt 7/dt2NmQ֔uլYR{sxP@hWW__{)pa.mט_|+( ` m;-zOv>.2P߈nil <|oўH0kncN@IDATLo.´P!*i!k=ʭV2uN#I 34u rR;=]_>[FM(SNUs{ァ.ޣPT v5>b آEP֭-p'V`sz_!*oTƼ +R^a]tppl &ԏ_aJRʀUDl5 |wl!voeŶu_'L 6_Q>qN}-K{Z7zG7U!} wiR1"PPk-@=ȸO԰Ei)c P~Aȃ ڂ)xJJߒ$Oݡ_+DO,+?$@$@$@$@$@q(wp׭-@)^1/!C0 @##vѣxҥ2灪ˇǗ>gv>:u2]BUlS]7*7ꗜ2]dƧǂ۴&*i%2cb P^zt{-@ 8q#mF/O>W-KZ.rG}d2b<ɐ! ̉1{Wk ڹbo߾GNP!P@X4jܨ*tok P" [w1^8?~H}o PC(k0n%f5=ۼTkiNd[\ "}8T[m'P#S6*a[OC׊|[eʅ:L Ѓs=u~+]t*} P,X\'ӱP;JѢEC;txf P?{-%K4[^W;>SwD GJ6aȎ+cZjãFx-_\ح؞Ev -@9yO`hmd(W^cl5`9Ohe PiQ)go_<-|:- ^t0ebZbX'9B_FdUX/E˥QJϰo)yҺ:+XJxUM**g/\?BXvCTJ"eLↇ![z{R,Bn% |G4C'UB҈_݅F,R69ոFJt(n{6^~|Uzle29rbT(kop"c PPlʸ8/sxHÊ!7+[Dd6%@YndHQwڮN%nIHHHHH@>@a;vᇘ/i*URƼBcFFPKҨm_:"ۦ]>* w`B PlO,3g}7;~!O=Goݺ[yJDfLl!Dx0)%7xY7z\mJdD^/JV%b2 4a4 2l {f͇B5_.yW?21m2ɓ`(^bwm%gNUePOw:6 PZtwsC;',Oy!H>Wy FmQFt<آ @鈄oQyG$@A_l !mxO?Lxu?IU&J;L>}YxgE]vx5w/9_|-@YzoKTL>>˺v* և;mԨWnLb)^RΝjݘ!;&o6jzkWl[Ux ykPPҧO%3p؟>}I uK2=TtgY`(#n7Pam((gJ$HʛK+gt:l޽{5, ~޽[ `c? cGB7xCΟK6; /e)R#F7oW :L\Ђ,blΝ;gOo0B͘0*7V^|o6By6nܨĈ l+W'29hVM)s=.]Ⱦ}!U[q1iݺ;(ݻK9^zB^6vXv.>:lH^ɻ]s-ELl9f\ Bо6HWDfo7].E%PKu<Ą=tԳ˶h#2o9&C~Yg0J vLY9<[y;YGn :cMFϕdH{Tq$P#v/X<ߜ sLo_ɷ( | mTQR&J753[Ey <yٴvxwC0z/2rߡωTxmj:Io%K2ݷ3WNK%!1HHHHHK;`}AgB$$6dpsy:kyMNߗxpkV$Ætm.?}b`'^%-:"Hl'Cf' Avw%[Hs#ҥދ%6vڀ7\6(W1%E$:,a:kZeM4,\O}rNȤfڵۜvފD܈ɗ>xcSE 3Lp2fF$yf-}XgR!LM>Tz vɌ~ K7h-"cY.^D02"#@BT~1]CFm3A2ieIPbuޟQuhcX`|{xB5C2U I^}mo[ UdZ4Vbz\m-P9&# ޟ%n7>(s9rf|قVR"N֭QNj2nL !@!$ܾl#Y dQr"0m7J~or;[^w ,KxVniiP.rZw/65Ko܀]hԨԪYRw/N|x~|o'[=ŋzu@PѰaCzD Z-^XMnV| S{ aNx0c%H -p:|#q-W+T@wSǏVZ5pp˗iСCZdE)S&'  ݤy 1|~|s%<&/c缿3P۠{HHHHHH@ M{ !œ) &\ G3gt7>c?h1CnΜOl!q @QP~r]-'IP{ / [۷ȑ!ɝ;`_**vq7쨑ms.6[pW7}jv ēO8Yhg @{D{uovDW"c_}tz >X޷`AȎ +Y]}9k=zVVaZlr\C%@EZߟ@&Mx01 9sgfWo X,Ǣ9 aòwQ9t蔚)սW纀jmѝВ*ƶm{dʍzaL'U}.}M1?l~uӔZK .V+< Vo!C̎+\MϨ?-[wI,F%U(-&5-%s7-3 34u`;qBg%v-a11Ϸ/?w]2 ' O5x$sqB`  ð b)@Aڴi#ŋwwYyj^*U*Y!(QBǏ/}U\wʺTP>p 4ѥKt8mC M69EɓuQqy$stҥjN~+=H)PL4|ᇺ+J1H L0I$Q}oZ ȑ#&+6:?L,Y8y! : s"1K m@dy .ٳgW YI&*ҷku"; G=Z{+'5%죻WکdN6<9~FEvMO;qV  >B|8ɔN+[P >Q';~9*'T(ʃ_8e  Sr'!g?,IX]_ɚ6_?"ܥJOW._EΪ`gliO[dmT6%ds9 }W6:$c/.YW(@)0O߰O(i$z\Y%C*!xCErd<'$msHp_V3(EgX 1:'FHiaKXiSKem?+/Aud'uzu l!޳VP;ρv] \/_JSIq7-@ |a(s5Wʾ*O'B~pnCF;Dd̎h˴{+()O!_9ii[     #@ػ8BwOlD(K1c1#dqFsൠ^]ߛa:lg]8ucG$@AQH\Yv W+ZA>Nǔ X4m},֫?B'|dǤarRK0[KB5WƁ'YazYl6N0[[GFdưo EwBD5zT[of{Ĥ-ڡNn l* P˧8f>%:EWkK&w|#!<4',P^zt\i֞3xs*#>{c0֐j[?W)^BԬY3`" p2_} N%OCxs'KN{0cK\wn 8.\xϟ^UU-@7U_~)#F:#@p2#P#Νz+\'87j{'X(^orcvƍaPx((v{4B 0@YEG7o^ӧp!dk޼gx ȶ1cƄxǑX_ MǼ#ܮרo'Ef8CTXX׼(nCwүz}@ aye((J,7)FQ [ sB$ڷSbxDz1iT't #},z_ݗtzST<2~y'Q 飫:cGz(@$@JN~P,*\DF!ӝ"j0޽|oxLB1vH1c^dK}9޾;v_@ !uxk{W\ :)=ܯސ d˴9i; X9'OmPy4tl5b{S9yWر#GW!&hѼ0ٳ(S)1`+F}"a.ket+BPaYW#߶mt2&/L}@0vy#3iӦ~)l>(?gҹs0u &|e2''Ojqǔ-ZT%PT[*晇q׍`y iԨY}<ԩSzpsߨOkW Lnʈ&E>XPyy`K.O QHI(3^N +vYXm,lo{D~CcZU,.YHQ(o!O(~*/:|R]2CV} #2r\RBaQ/wQ>1gTj'Sb_0/%_j/(fP^?Y{X-eRPmdx 1uBTqQf PlQ;zJ{DyJWwzq3VpQHѧ+φc'_3WKo{˃)}_:NZ"~q;3IC>he S"Ò#MOq(2-3 0?eTʛ0~@x_N*FÅ#/ڏww!b#]Ruc,&?& K,Q!k!I4it~ހNj4 C "@[ kx\V+Ng/{E(޵JSxk)ɇ|a? '.&+zIHHHHb? Pb$@[ӷorl ?j7x}ã ܒ i-[%Jk7ސ~-N_sL^\{Ncƌ;1% Bvϵ!.eT,:Ȯ9-K2Ix>ܾK.Bw2_cxw˴v}pZ{⬜Wa%J EgD " L#     B2 m'7o61_?:ݙv- 8KRJRX1Of͚gV\X޼yA_ !(@$pڶY~:|R,LOxxH ĕ(ϽK T$@$@$@$@$@$p[Pr[Q H k2ft[ݵ?U{_֭bcw'  @vpº| 6l  @^L\$ `W%g.\%w=cCW+ދE<t|wKӏIl$cxPsx嫲9&~ @l#@JlHHHHHHH ,/L.3Hb#Ў y'k#    9[L$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@ 'I$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@1Gc˚I M}} lJEH*!dKGd͒TR,-P%R)}gc?;ޙa{y#,s<Ͻ3       76s         rl92        pC@!n3       \=(Wϖ##       76s         rl92        pC@!n3       \=(Wϖ##       76s   p$KBRd!)3i2șkBIO!Y\bNXǼ    $TJBh@ߚrr.朴\ + #ӒvX||"3XuELD~rikEX~i$C^rr^Nrr`+r.rZo)6 <+'"څ     p @oK*Stu2e|b]"yM_n#G~sOEyauӟ,?:hׯ&[=o~?^w[:dȐVN<#OngBkЊ29:Ll'|/zGW@zȝ;t\چ dƌ9k=+9k='7(?kH%cmPDJԯ%K3>|k+ %z$S][~ZZ[]:]8{`l{aVLK&=[d @@@@S(, :ܬ޼ytoYVod id@^B 4׹89ph}i!a,v!۳]F{_]'/ 6/>")R0x8O ],Y2xԮ#LאOGI2y%s1F:n+Qg!ϥ -PZ5yEܹsٹslڴIVZ%'N7_ *ȠA98 ;wNFOrz~/7YTζHi2F,E &G6mwi&KƒOɾ9u"wN[}*$K.SP=)yu.wrj_r|f]j4]Y{1>Ǣ     o 5oڵȋZyccysr *wmD:Ιs"2yz-(Ӈ.]Ɇ +IʘѝrMߝ4Gf\_Bk|mSxќ:Oׯ/:t0]y %cƌ+ڵ(H͛H%qU$|2c._RPM&g1׾;ሤJ$*.~|'A{U2T> ?G#S>;퐞 bWIi    a'@%nuoE+t5=n}0hn7 37fmﻱCEf@Y8I2?#uܫ<ծRbIs&ϓO?wO;zU)9Ȟ>C=$I?vx:h'Ht̕o^"##PΟ?oMڵK^|E9}=/S >gϞ=+'p=P4Bdd.x$ĩ]UjUkhV3c%r[qRw`ݟwOΚ$u!~x;m9GiY(DaB@@@@G r xhw%)޽tP.]ٵKʵ}1oƼ2gH܎ : op *x_Kz_gO* ,Y2Hdԩ3w!֗<6U4mRK)37ъ$'d_̰)잪U++#?cViM75KrO|{zg9(ڗm;u|5GYw8篤ʕܓ ^r:O<#LߊysHͥT'{Bdd*⋥rl=X*v.o*g }o{=1w-.~(ruʔ~eP{n=zJPt,GGG ݸ[nRD zjwQNiذϟ_RHamǎoÇ[Mu5mKM6r}E}u_֯>pʕEܒ>}zZ}+&M틿J17nl^%9r0齃9vy?EaoJ.-]v5?>gyT/Q[z;ѐWE|V[Æ =YF +͛77͚5K-ZuY1`\L2Žʙw;~FQF9]^z%rmj_t"ŋw~99x,X@.\kP_Y3K%gْ:w[gcoc;z<%{Yo*?#|~F#}m2qgĭ-*IΝ:-X#Θ#gڿ5%_eO kj 9}$wp~t w߲L%O2`>d @IDAT~f2[˵n$,T9rY7:c~:k](PC!E3}{>~    ׯ0׷`=Pu lu6([ХB9+ˋ'Wyw4B#Pߤg|ifR!k;wvO9K:{JSO5Oqڙ_J3f \=I7 _D:=$_P'XæYW{5GyŵP>&J){ |IitLyd[=jRJimϞCҦptȺgDlzꩧAڦAP[zuyiXwЇ_|a6O z&T|UI^ޟ?>?(xhXg"dacM%c\fqirG>Wv{E?fyѿ*w:{AɑMX;zu0$)3E j+vR|T(#whB< .\k7Jf+뾘,rV%Cfse7#c#>+W~F, tF@@@@Mۜm}vH}TGȓ'owEPۓ;b=lַX#qe_RvU{Tؾ=Bb*Esί ƌa}+_|>xh_NJmM$"ⰴl ` w'jSbP<ᵊKԞ*Lt:zX]Aǹ8fޚëJΜ{ﵸ(ҥ_rVٸi=r\gl=f^ Æ(˖RAn (pOZ$P+nOGYR[՞&[A3ۋܯG{}8j}c?R+3zm7+R6-2+c;g]V@3f4õJ5h}gMg2m75d3+ (ZC3yʛoYnj3& _ nw;bwGkF+}R=wP4A'?N26 }(Z}8zVIg ZuF2%E%M)cV89hLX*0啷e?Uj=;XP. RܻE*RvU}(w; nhV4Y>_KVuCJIEͮ;k,>L> NDv?,ׁ)=|[^^20]؎    ׉0gRI^#/ToЇSC^~JW4?s&ZyEޕZ-{vJkKau]ld+UP{ڥn!JX7(]LFn }G&f[>-j"X/wǛWLu*vDker=կ_\&y>V!4{/ʦM̃z c!m&7Ùh(P*Cs1@k @0/bѐ7t)C褁 aAh:Jhdv… 㝣-]Ԅ(*Tƶ!4 ]^x hXB/2n8 h`COL|'wަ ٓhbSA0aZR+VwP=!Pio cOq?|(~^ǿ5%4{DL$U]e}y*VY3>{dһok@n^^ۿ5Sm_>9+Ұ#d%C-7A=RH@NE' 6]NP>NTb5h&;O?w:nr+"RգRoĈUʊ]MbP~6At}sjx3,>LѪ%G T@&淝R++}_u%1^C(PV Rz[J/s|jWٽaM"˜ѝr(Oo-oij0W 2P*tJ *TTD0ZC'8p@Y~<$_39sfkI1|CJNŊ{C 7W{R ڋΫoEC6=3 nj#7t9͒%Kdĉ^t:LѢE>~W}w|    ס07{53G|<)Pw*P 5 ShKClNJ.⻿e_v:~߷pnG(P =':i La:ȑY|SV;x93;yliYYKv5 :?P2fTy׬ P?޸6^NP}aX@m&`/;;3fpjCi[weҭ[7!λC ,ɓ3ʇ=S˹LaM!a{rPUɛ78qlkn*ˁ,74vX_3-v&4rw;kcv'{%`I'څ¶VhEV:9}$x-1(>g=GB/7;B32֩e*&z~9zLv)z]Rf؎=fgkF'h7e]ʵȩȡߑS۽+(y5s!ddDͫuڅw楿ռ6r%L>s]ՔY@@@@ uWDhvL{caܹɧ37 mrO)M[&HS-~;MSKC#mz @}nv\ĺ'  +#;{etySugOD+5BE\oc=OmVgj;ϘƇKzt<Љ+r_w_e `?>n֭+<׺-P%Ba@;wWw4bWwy뭷dҥf@*Zm#>SNIw7RV-mҷoj EZjk ʕ,jB+l۶̈́rm{IeORdI5RΝ;ˁ8O>lPU}RHaNu9vZ-rƍmmI@ыܹvZczL>qo 6~] ʡuފu4Y2Ƀ0F_rڸ3ڿz2]g{JRfU_%M֐;$UT-bIg߿&_nMƛ'vȕK̉#r>*B7|-&[o~A8}l@@@@@KJOw@c/ITTs}iV%v#Xf`CcwϞ߽W5+UunWg׮y1tG FC%z_] v+ޓ@@|OҬ}Ҵɽ֐|7eXLDZ~q!5gDK^p_3߰auޭY+R9 v[~f=_:WL *;W>}Zj&vFkP`ڸC"Z믿uiJR|wtWpŋ}e hhA]vI*U*awiӦr{u„ ot?=t("z@ we߾}]ׄP$˛+h"Ef;C߿ܹs2gΜCiCRPԠHjrs!w$1V5﬊ ^/Upr(lFNu`m2DFV~uEVFNzJr? ]#}MȔ*K~ǥe 9i{[=#^ >;     @x @ 殖煷eժ PÂ^iSOZqXZm *˗c [2$G?x3Jb }2g իj*JŊ%jtmӣ~_5 m[Oڴ| dO]tq:-Ϳh,p2d?.7m *P+rhe) j@Ѫ&Zv1bkUfʢEދ =g|& =7RJi2lX;o^"##k;T+4p%wo`ZP:u$u1Ea@|$Fe hDC(:i8e˖O?ԩf?^k8.'eJ.亥Z2]{o8 gLbP%v;u2RN 𔧂Y]쪄:K p*ĺd)ԋHԞ!;~DG&,Lm)9{`l{agE[Ir-ME{h    ׁ03>%Kf{7? {^ʜ{ 3~ɟ2rDZpX*L|+=#Ջ4yckeltDDt5Xst蛆 M=~x駟O̕* :>}ZZjuEw-F|&}k"k֬]]Gqk}yHwCk q'w%&&F5k&oTF ѣYۚ4i_n;<`=1홴iӊD]'!@Ww% 6Hc^Z!I+h{Fy=~z ^ϻj˩իu]l(XDAʕ~zӕNIYP >ZF3.U=y]wֹ$ZD+$drnK/-|rh!4bO ld^i!9u66eZӬ-Dva;    \'PF,KMACsN:GϞ=9E+DFb˗믿z|Nt:h(uh⩧2CIj̙#}KZ;HΜ9ͺ@ xܹsV奔@;(zAuN>*߿0JΝ:m5YRkX/Zs@)3}7^s.@@@@.m߿Ȃӹren ^UҤI-}+q@sNd͛CgO?c$ ҥ KʉG x? **3f ːܴIZarQkH7o5!Ҳ0'JtiYÌsIit\p>DPT\NY&baX @ N>߭r1|)+`󥙷I{uL}xگvO⵭nۭ 7]ٳ2μΜ>}F.YC3Mz?mM[챆\($w׬(g dl٢2:tL? ./U!1yCCHT)Mkn]-u.ǹ.O%$?qi5>w8g}&"W+/`X%KXC$eE׽{wYΚwߙ%9r䐲eJZi OCMc^֭[E&RpO;vN=ԩSumL8ig&Pk,[̪E{1=igǎP /WZ֫WT9W\h˹o2dȠgܹs寿B Vɖ-٦<~o`4i9o~XڶTҟۭއL2Y?GR9 h5;wzw/\I%]tr}'Eui?S 4t~&4$h&w?2ϩ,YR*Ud?ߦL"wk>>k^BRPtn%]tȘ~ɉ$SR jI2U/cug]k--vOo*']$X(EKN< k`:U`)S 6,}WsYf9muȑ#_@ϲ}]?ooYw% i ;$g}6h7>nB)Rb w@@U9r:<|v%[oU IX% urwZ%HDiĉ^:bĈ&hr\kRPǽ09Of }pIroya+ ;WuAf m%FEOU;3Cnt7AJ/-D)w>5߿&{BoqgoOu8lG!aN5g=3g3C#h#7 >hٳ$CÇLe֡}7ieN5GDɔdqV--^dyqS{nnR@D1YjiZV%8mq"n^t]nMaAn7ac-kd?_NWzODY@vkN 9}4hp){fkcs8f׿CZ+yduz{[{!1 cRM6xJ":V364|XBn'VY|NNj0cL2屏$Er!uO}e(U"/nޓEWyCc bә3g0z_[BϯKĠ I~͚5*O= ӧzDvE _+DFFJ;:N z߃O>Df6_b,(z{Tp[Us}IhA'@ALz,:DR> }#Ç=۴Jɓ~>gҷo_f^y{ {GvT}kjLݦCQu_3>CB&Mj;Li߾SYwȢՎ>À_]NV(۬d̗gSf#bJ 7CS>rL:0Z=D]r|O쟑vw5MEm~c$rn-o[:Gk43ZfdaגlI)߮dʗGt蝋VXlR hef>?9j/x49sJ.^_$ytRbI1Nrb`~ҡf?ȗk7AX    a-@%o8B2ELd=VM*:\K̗/6Rh>3L̎nv3M|ϫt JHe}f;0!%=IJԩSY禍R`nɛ'T[5Iٵ$=ĢN9吏j -cÏҴxQҪ'|Ɗ_b4 0m4ѡJja=ʕ+g*$hDXfMU2Պpݞ|e_WwEג1cF$ɕ+~fݺu $F-b v96R[oP!1P *Cݻ7q.S@?Сw4 w")?k}ewŽ=jB;sm)3W9CU/УlVx#C>g63d?40]SW0?~*)źs]>%g#'ˡ%كm:X5~G[I)SZLckhe%QɴǦKTxq0 ?@@@@@ r}Gn0Iv}7_|&}@4GɪrQyzΓ>4hE!j []UEwkx8 PMK @9G5iH,Y$$L|+pI`'tn듧ݫ$$w~^t!b`X냭xCҾ3kw<;@@@@@ = @:!tٳ˅ zr2tH{N;xb<!tBY KEPtx{לSywe23 Q3gt%aGKxk( 7cHϚ?ًHtn9XuE$SO,wtq|]mO\RCG6oC:-R>`֟>|Tyvp6VVX2\KRe')2i3oG˙(}3TCW{RbFIGZbNX?skE!    \_P u"0vL3UL3ю&lPp^K#?͢STTk.T 0ĉeɒ%՞!r9~$g-TG g;NCk7ɱ]$ut֛%s 6A,3    P; nX; `|7`Bb[^$o޼ʬYdʕ!ѯpĭ*O>>ȑ#k9@ڜ+)ι@ v _}#f.քm     @H @ Ag@@*Iɒ%}"Er̙h9|8JfZ*s I"гgOR9;#˗/O~pR@ \ U"kT%HtacĞY{_^F@@@@T zl?dɒSsz A/M؈    PC@@@@@@@q(!~        PB?@@@@@@@@  {        @ @ ;D@@@@@@@@J{=@@@@@@@@(> ,"       $LJ¼h        #@E@@@@@@@@ @I@@@@@@@@|        0 K\ߝYgeh5        J2W>/Z        @ lZ +Eb\&ٳ\m@@@@@@@@!@QSH)N+B<:e2#Q^3"       -4o=#w<        @ MA '6&E@@@@@@@p MRtkL#H7y@@@@@@@@$Jdx(I>        @@(i؀         DM'>76        Jl)eŌR#E)`ն        ^ƅe%.=2gɱx\"M@@@@@@@@@XP>|U1`;òxqٺ묜?QNҵL        R ($53KTv-9+R!\7B@@@@@@@T (FJ9XԺLP f@@@@@@@@k*V瑧pvED7?;JdT,@@@@@@@@k#VusHwVsRqm8         @@ dϒB~fB6`8+A@@@@@@@H T(Nf/fN /I8)        @XP"H7x_ K        I"@%I9)        pMZ +Ec1r{s*\?oC@@@@@@@Y l({{Zo gw       \7a@ILd7Ii NJ;        ,S˻ It%W֔*B4h{W@@@@@@@@$J2cѬrF}vk@@@@@@@@H (FZXD_C2}QYTHqV@@@@@@@@!@[V"       %t@@@@@@@/(u-        rPB!@@@@@@@@         @ @ [B@@@@@@@@ ^"       !'@%n B@@@@@@@KJx/z       %t@@@@@@@/(u-        rPB!@@@@@@@@         @ @BFIDAT [B@@@@@@@@ ^"       !'@%n B@@@@@@@KJx/z       %t@@@@@@@/(u-        rPB!@@@@@@@@         @ @ [B@@@@@@@@ ^"       !'@%n B@@@@@@@KJx/z       ‰qIENDB`Proton-API-Bridge-1.0.0/utility/000077500000000000000000000000001447740121100162765ustar00rootroot00000000000000Proton-API-Bridge-1.0.0/utility/init.go000066400000000000000000000001461447740121100175710ustar00rootroot00000000000000package utility import ( "log" ) func SetupLog() { log.SetFlags(log.LstdFlags | log.Lshortfile) } Proton-API-Bridge-1.0.0/volumes.go000066400000000000000000000004241447740121100166140ustar00rootroot00000000000000package proton_api_bridge import ( "context" "github.com/henrybear327/go-proton-api" ) func listAllVolumes(ctx context.Context, c *proton.Client) ([]proton.Volume, error) { volumes, err := c.ListVolumes(ctx) if err != nil { return nil, err } return volumes, nil }