gog/
lib.rs

1//! This crate provides an easy interface to communicate with the not-so-easy (unofficial) GOG API.
2//! Many thanks to [Yepoleb](https://github.com/Yepoleb), who made
3//! [this](https://gogapidocs.readthedocs.io/en/latest/index.html) very helpful set of docs.
4use serde_json::json;
5mod containers;
6/// Provides error-handling logic
7mod error;
8/// Module for extracting GOG installers into their component parts
9pub mod extract;
10/// Module for GOG structs and responses
11pub mod gog;
12/// Module for OAuth token management
13pub mod token;
14use connect::*;
15use containers::*;
16use curl::easy::{Easy2, Handler, WriteError};
17use domains::*;
18/// Main error for GOG calls
19pub use error::Error;
20pub use error::ErrorKind;
21pub use error::Result;
22use extract::*;
23use gog::*;
24use product::*;
25use regex::*;
26use reqwest::blocking::{Client, Response};
27use reqwest::header::*;
28use reqwest::redirect::Policy;
29use reqwest::Method;
30use serde::de::DeserializeOwned;
31use serde_json::value::{Map, Value};
32use std::cell::RefCell;
33use std::io::BufRead;
34use std::io::BufReader;
35use std::io::Read;
36use token::Token;
37use ErrorKind::*;
38
39const GET: Method = Method::GET;
40const POST: Method = Method::POST;
41
42// This is returned from functions that GOG doesn't return anything for. Should only be used for error-checking to see if requests failed, etc.
43pub type EmptyResponse = ::std::result::Result<Response, Error>;
44macro_rules! map_p {
45    ($($js: tt)+) => {
46        Some(json!($($js)+).as_object().unwrap().clone())
47    }
48}
49
50// The main GOG Struct that you'll use to make API calls.
51pub struct Gog {
52    pub token: RefCell<Token>,
53    pub client: RefCell<Client>,
54    pub client_noredirect: RefCell<Client>,
55    pub auto_update: bool,
56}
57impl Gog {
58    // Initializes out of a token from a login code
59    pub fn from_login_code(code: &str) -> Gog {
60        Gog::from_token(Token::from_login_code(code).unwrap())
61    }
62
63    // Creates using a pre-made token
64    pub fn new(token: Token) -> Gog {
65        Gog::from_token(token)
66    }
67
68    fn from_token(token: Token) -> Gog {
69        let headers = Gog::headers_token(&token.access_token);
70        let mut client = Client::builder();
71        let mut client_re = Client::builder().redirect(Policy::none());
72        client = client.default_headers(headers.clone());
73        client_re = client_re.default_headers(headers);
74        Gog {
75            token: RefCell::new(token),
76            client: RefCell::new(client.build().unwrap()),
77            client_noredirect: RefCell::new(client_re.build().unwrap()),
78            auto_update: true,
79        }
80    }
81
82    fn update_token(&self, token: Token) {
83        let headers = Gog::headers_token(&token.access_token);
84        let client = Client::builder();
85        let client_re = Client::builder().redirect(Policy::none());
86        self.client
87            .replace(client.default_headers(headers.clone()).build().unwrap());
88        self.client_noredirect
89            .replace(client_re.default_headers(headers).build().unwrap());
90        self.token.replace(token);
91    }
92
93    pub fn uid_string(&self) -> String {
94        self.token.borrow().user_id.clone()
95    }
96
97    pub fn uid(&self) -> i64 {
98        self.token.borrow().user_id.parse().unwrap()
99    }
100
101    fn headers_token(at: &str) -> reqwest::header::HeaderMap {
102        let mut headers = reqwest::header::HeaderMap::new();
103        headers.insert(
104            "Authorization",
105            ("Bearer ".to_string() + at).parse().unwrap(),
106        );
107        // GOG now requires this magic cookie to be included in all requests.
108        headers.insert("CSRF", "csrf=true".parse().unwrap());
109        headers
110    }
111
112    fn rget(
113        &self,
114        domain: &str,
115        path: &str,
116        params: Option<Map<String, Value>>,
117    ) -> Result<Response> {
118        self.rreq(GET, domain, path, params)
119    }
120
121    fn rpost(
122        &self,
123        domain: &str,
124        path: &str,
125        params: Option<Map<String, Value>>,
126    ) -> Result<Response> {
127        self.rreq(POST, domain, path, params)
128    }
129
130    fn rreq(
131        &self,
132        method: Method,
133        domain: &str,
134        path: &str,
135        params: Option<Map<String, Value>>,
136    ) -> Result<Response> {
137        if self.token.borrow().is_expired() {
138            if self.auto_update {
139                let new_token = self.token.borrow().refresh()?;
140                self.update_token(new_token);
141                self.rreq(method, domain, path, params)
142            } else {
143                Err(ExpiredToken.into())
144            }
145        } else {
146            let mut url = domain.to_string() + path;
147            if let Some(temp_params) = params {
148                let params = temp_params;
149                if !params.is_empty() {
150                    url += "?";
151                    for (k, v) in params.iter() {
152                        url = url + k + "=" + &v.to_string() + "&";
153                    }
154                    url.pop();
155                }
156            }
157            Ok(self.client.borrow().request(method, &url).send()?)
158        }
159    }
160
161    fn fget<T>(&self, domain: &str, path: &str, params: Option<Map<String, Value>>) -> Result<T>
162    where
163        T: DeserializeOwned,
164    {
165        self.freq(GET, domain, path, params)
166    }
167
168    fn _fpost<T>(&self, domain: &str, path: &str, params: Option<Map<String, Value>>) -> Result<T>
169    where
170        T: DeserializeOwned,
171    {
172        self.freq(POST, domain, path, params)
173    }
174
175    fn freq<T>(
176        &self,
177        method: Method,
178        domain: &str,
179        path: &str,
180        params: Option<Map<String, Value>>,
181    ) -> Result<T>
182    where
183        T: DeserializeOwned,
184    {
185        let res = self.rreq(method, domain, path, params)?;
186        let st = res.text()?;
187        Ok(serde_json::from_str(&st)?)
188    }
189
190    fn nfreq<T>(
191        &self,
192        method: Method,
193        domain: &str,
194        path: &str,
195        params: Option<Map<String, Value>>,
196        nested: &str,
197    ) -> Result<T>
198    where
199        T: DeserializeOwned,
200    {
201        let r: Map<String, Value> = self.freq(method, domain, path, params)?;
202        if r.contains_key(nested) {
203            Ok(serde_json::from_str(&r.get(nested).unwrap().to_string())?)
204        } else {
205            Err(MissingField(nested.to_string()).into())
206        }
207    }
208
209    fn nfget<T>(
210        &self,
211        domain: &str,
212        path: &str,
213        params: Option<Map<String, Value>>,
214        nested: &str,
215    ) -> Result<T>
216    where
217        T: DeserializeOwned,
218    {
219        self.nfreq(GET, domain, path, params, nested)
220    }
221
222    fn nfpost<T>(
223        &self,
224        domain: &str,
225        path: &str,
226        params: Option<Map<String, Value>>,
227        nested: &str,
228    ) -> Result<T>
229    where
230        T: DeserializeOwned,
231    {
232        self.nfreq(POST, domain, path, params, nested)
233    }
234
235    // Gets the data of the user that is currently logged in
236    pub fn get_user_data(&self) -> Result<UserData> {
237        self.fget(EMBD, "/userData.json", None)
238    }
239
240    // Gets any publically available data about a user
241    pub fn get_pub_info(&self, uid: i64, expand: Vec<String>) -> Result<PubInfo> {
242        self.fget(
243            EMBD,
244            &("/users/info/".to_string() + &uid.to_string()),
245            map_p!({
246            "expand": expand.iter().fold("".to_string(), fold_mult)
247            }),
248        )
249    }
250
251    // Gets a user's owned games. Only gameids.
252    pub fn get_games(&self) -> Result<Vec<i64>> {
253        let r: OwnedGames = self.fget(EMBD, "/user/data/games", None)?;
254        Ok(r.owned)
255    }
256
257    // Gets more info about a game by gameid
258    pub fn get_game_details(&self, game_id: i64) -> Result<GameDetails> {
259        let mut res: GameDetailsP = self.fget(
260            EMBD,
261            &("/account/gameDetails/".to_string() + &game_id.to_string() + ".json"),
262            None,
263        )?;
264        if !res.downloads.is_empty() {
265            res.downloads[0].remove(0);
266            let downloads: Downloads =
267                serde_json::from_str(&serde_json::to_string(&res.downloads[0][0])?)?;
268            Ok(res.into_details(downloads))
269        } else {
270            Err(NotAvailable.into())
271        }
272    }
273
274    // Returns a vec of game parts
275    pub fn download_game(&self, downloads: Vec<Download>) -> Vec<Result<Response>> {
276        downloads
277            .iter()
278            .map(|x| {
279                let mut url = BASE.to_string() + &x.manual_url;
280                let mut response;
281                loop {
282                    let temp_response = self.client_noredirect.borrow().get(url).send();
283                    if let Ok(temp) = temp_response {
284                        response = temp;
285                        let headers = response.headers();
286                        // GOG appears to be inconsistent with returning either 301/302, so this just checks for a redirect location.
287                        if headers.contains_key("location") {
288                            url = headers
289                                .get("location")
290                                .unwrap()
291                                .to_str()
292                                .unwrap()
293                                .to_string();
294                        } else {
295                            break;
296                        }
297                    } else {
298                        return Err(temp_response.err().unwrap().into());
299                    }
300                }
301                Ok(response)
302            })
303            .collect()
304    }
305
306    // Hides a product from your library
307    pub fn hide_product(&self, game_id: i64) -> EmptyResponse {
308        self.rget(
309            EMBD,
310            &("/account/hideProduct".to_string() + &game_id.to_string()),
311            None,
312        )
313    }
314
315    // Reveals a product in your library
316    pub fn reveal_product(&self, game_id: i64) -> EmptyResponse {
317        self.rget(
318            EMBD,
319            &("/account/revealProduct".to_string() + &game_id.to_string()),
320            None,
321        )
322    }
323
324    // Gets the wishlist of the current user
325    pub fn wishlist(&self) -> Result<Wishlist> {
326        self.fget(EMBD, "/user/wishlist.json", None)
327    }
328
329    // Adds an item to the wishlist. Returns wishlist
330    pub fn add_wishlist(&self, game_id: i64) -> Result<Wishlist> {
331        self.fget(
332            EMBD,
333            &("/user/wishlist/add/".to_string() + &game_id.to_string()),
334            None,
335        )
336    }
337
338    // Removes an item from wishlist. Returns wishlist
339    pub fn rm_wishlist(&self, game_id: i64) -> Result<Wishlist> {
340        self.fget(
341            EMBD,
342            &("/user/wishlist/remove/".to_string() + &game_id.to_string()),
343            None,
344        )
345    }
346
347    // Sets birthday of account. Date should be in ISO 8601 format
348    pub fn save_birthday(&self, bday: &str) -> EmptyResponse {
349        self.rget(EMBD, &("/account/save_birthday".to_string() + bday), None)
350    }
351
352    // Sets country of account. Country should be in ISO 3166 format
353    pub fn save_country(&self, country: &str) -> EmptyResponse {
354        self.rget(EMBD, &("/account/save_country".to_string() + country), None)
355    }
356
357    // Changes default currency. Currency is in ISO 4217 format. Only currencies supported are
358    // ones in the currency enum.
359    pub fn save_currency(&self, currency: Currency) -> EmptyResponse {
360        self.rget(
361            EMBD,
362            &("/user/changeCurrency".to_string() + &currency.to_string()),
363            None,
364        )
365    }
366
367    // Changes default language. Possible languages are available as constants in the langauge
368    // enum.
369    pub fn save_language(&self, language: Language) -> EmptyResponse {
370        self.rget(
371            EMBD,
372            &("/user/changeLanguage".to_string() + &language.to_string()),
373            None,
374        )
375    }
376
377    // Gets info about the steam account linked to GOG Connect for the user id
378    pub fn connect_account(&self, user_id: i64) -> Result<LinkedSteam> {
379        self.fget(
380            EMBD,
381            &("/api/v1/users/".to_string() + &user_id.to_string() + "/gogLink/steam/linkedAccount"),
382            None,
383        )
384    }
385
386    // Gets claimable status of currently available games on GOG Connect
387    pub fn connect_status(&self, user_id: i64) -> Result<ConnectStatus> {
388        let st = self
389            .rget(
390                EMBD,
391                &("/api/v1/users/".to_string()
392                    + &user_id.to_string()
393                    + "/gogLink/steam/exchangeableProducts"),
394                None,
395            )?
396            .text()?;
397        if let Ok(cs) = serde_json::from_str(&st) {
398            return Ok(cs);
399        } else {
400            let map: Map<String, Value> = serde_json::from_str(&st)?;
401            if let Some(items) = map.get("items") {
402                let array = items.as_array();
403                if array.is_some() && array.unwrap().is_empty() {
404                    return Err(NotAvailable.into());
405                }
406            }
407        }
408        Err(MissingField("items".to_string()).into())
409    }
410
411    // Scans Connect for claimable games
412    pub fn connect_scan(&self, user_id: i64) -> EmptyResponse {
413        self.rget(
414            EMBD,
415            &("/api/v1/users/".to_string()
416                + &user_id.to_string()
417                + "/gogLink/steam/synchronizeUserProfile"),
418            None,
419        )
420    }
421
422    // Claims all available Connect games
423    pub fn connect_claim(&self, user_id: i64) -> EmptyResponse {
424        self.rget(
425            EMBD,
426            &("/api/v1/users/".to_string() + &user_id.to_string() + "/gogLink/steam/claimProducts"),
427            None,
428        )
429    }
430
431    // Returns detailed info about a product/products.
432    pub fn product(&self, ids: Vec<i64>, expand: Vec<String>) -> Result<Vec<Product>> {
433        self.fget(
434            API,
435            "/products",
436            map_p!({
437                "expand": expand.iter().fold("".to_string(), fold_mult),
438                "ids": ids.iter().fold("".to_string(), |acc, x|{
439                    acc + "," + &x.to_string()
440                })
441            }),
442        )
443    }
444
445    // Get a list of achievements for a game and user id
446    pub fn achievements(&self, product_id: i64, user_id: i64) -> Result<AchievementList> {
447        self.fget(
448            GPLAY,
449            &("/clients/".to_string()
450                + &product_id.to_string()
451                + "/users/"
452                + &user_id.to_string()
453                + "/achievements"),
454            None,
455        )
456    }
457
458    // Adds tag with tagid to product
459    pub fn add_tag(&self, product_id: i64, tag_id: i64) -> Result<bool> {
460        let res: Result<Success> = self.fget(
461            EMBD,
462            "/account/tags/attach",
463            map_p!({
464                "product_id":product_id,
465                "tag_id":tag_id
466            }),
467        );
468        res.map(|x| x.success)
469    }
470
471    // Removes tag with tagid from product
472    pub fn rm_tag(&self, product_id: i64, tag_id: i64) -> Result<bool> {
473        self.nfget(
474            EMBD,
475            "/account/tags/detach",
476            map_p!({
477                "product_id":product_id,
478                "tag_id":tag_id
479            }),
480            "success",
481        )
482    }
483
484    // Fetches info about a set of products owned by the user based on search criteria
485    pub fn get_filtered_products(&self, params: FilterParams) -> Result<FilteredProducts> {
486        // GOG.com url is just to avoid "cannot be a base" url parse error, as we only need the path anyways
487        let url = reqwest::Url::parse(
488            &("https://gog.com/account/getFilteredProducts".to_string()
489                + &params.to_query_string()),
490        )
491        .unwrap();
492        let path = url.path().to_string() + "?" + url.query().unwrap();
493        self.fget(EMBD, &path, None)
494    }
495
496    // Fetches info about all products matching criteria
497    pub fn get_all_filtered_products(&self, params: FilterParams) -> Result<Vec<ProductDetails>> {
498        let url = reqwest::Url::parse(
499            &("https://gog.com/account/getFilteredProducts".to_string()
500                + &params.to_query_string()),
501        )
502        .unwrap();
503        let mut page = 1;
504        let path = url.path().to_string() + "?" + url.query().unwrap();
505        let mut products = vec![];
506        loop {
507            let res: FilteredProducts =
508                self.fget(EMBD, &format!("{}&page={}", path, page), None)?;
509            products.push(res.products);
510            if page >= res.total_pages {
511                break;
512            } else {
513                page += 1;
514            }
515        }
516        Ok(products.into_iter().flatten().collect())
517    }
518
519    // Fetches info about a set of products based on search criteria
520    pub fn get_products(&self, params: FilterParams) -> Result<Vec<UnownedProductDetails>> {
521        // GOG.com url is just to avoid "cannot be a base" url parse error, as we only need the path anyways
522        let url = reqwest::Url::parse(
523            &("https://gog.com/games/ajax/filtered".to_string() + &params.to_query_string()),
524        )
525        .unwrap();
526        let path = url.path().to_string() + "?" + url.query().unwrap();
527        self.nfget(EMBD, &path, None, "products")
528    }
529
530    // Creates a new tag. Returns the tag's id
531    pub fn create_tag(&self, name: &str) -> Result<i64> {
532        return self
533            .nfget(EMBD, "/account/tags/add", map_p!({ "name": name }), "id")
534            .map(|x: String| x.parse::<i64>().unwrap());
535    }
536
537    // Deletes a tag. Returns bool indicating success
538    pub fn delete_tag(&self, tag_id: i64) -> Result<bool> {
539        let res: Result<StatusDel> =
540            self.fget(EMBD, "/account/tags/delete", map_p!({ "tag_id": tag_id }));
541        res.map(|x| return x.status.as_str() == "deleted")
542    }
543
544    // Changes newsletter subscription status
545    pub fn newsletter_subscription(&self, enabled: bool) -> EmptyResponse {
546        self.rget(
547            EMBD,
548            &("/account/save_newsletter_subscription/".to_string() + &(enabled as i32).to_string()),
549            None,
550        )
551    }
552
553    // Changes promo subscription status
554    pub fn promo_subscription(&self, enabled: bool) -> EmptyResponse {
555        self.rget(
556            EMBD,
557            &("/account/save_promo_subscription/".to_string() + &(enabled as i32).to_string()),
558            None,
559        )
560    }
561
562    // Changes wishlist subscription status
563    pub fn wishlist_subscription(&self, enabled: bool) -> EmptyResponse {
564        self.rget(
565            EMBD,
566            &("/account/save_wishlist_notification/".to_string() + &(enabled as i32).to_string()),
567            None,
568        )
569    }
570
571    // Shortcut function to enable or disable all subscriptions
572    pub fn all_subscription(&self, enabled: bool) -> Vec<EmptyResponse> {
573        vec![
574            self.newsletter_subscription(enabled),
575            self.promo_subscription(enabled),
576            self.wishlist_subscription(enabled),
577        ]
578    }
579
580    // Gets games this user has rated
581    pub fn game_ratings(&self) -> Result<Vec<(String, i64)>> {
582        let g: Map<String, Value> =
583            self.nfget(EMBD, "/user/games_rating.json", None, "games_rating")?;
584        Ok(g.iter()
585            .map(|x| (x.0.to_owned(), x.1.as_i64().unwrap()))
586            .collect::<Vec<(String, i64)>>())
587    }
588
589    // Gets reviews the user has voted on
590    pub fn voted_reviews(&self) -> Result<Vec<i64>> {
591        self.nfget(EMBD, "/user/review_votes.json", None, "reviews")
592    }
593
594    // Reports a review
595    pub fn report_review(&self, review_id: i32) -> Result<bool> {
596        self.nfpost(
597            EMBD,
598            &("/reviews/report/review/".to_string() + &review_id.to_string() + ".json"),
599            None,
600            "reported",
601        )
602    }
603
604    // Sets library background style
605    pub fn library_background(&self, bg: ShelfBackground) -> EmptyResponse {
606        self.rpost(
607            EMBD,
608            &("/account/save_shelf_background/".to_string() + bg.as_str()),
609            None,
610        )
611    }
612
613    // Returns list of friends
614    pub fn friends(&self) -> Result<Vec<Friend>> {
615        self.nfget(
616            CHAT,
617            &("/users/".to_string() + &self.uid_string() + "/friends"),
618            None,
619            "items",
620        )
621    }
622
623    fn get_sizes<R: Read>(&self, bufreader: &mut BufReader<R>) -> Result<(usize, usize)> {
624        let mut buffer = String::new();
625        let mut script_size = 0;
626        let mut script_bytes = 0;
627        let mut script = String::new();
628        let mut i = 1;
629        let mut filesize = 0;
630        let filesize_reg = Regex::new(r#"filesizes="(\d+)"#).unwrap();
631        let offset_reg = Regex::new(r"offset=`head -n (\d+)").unwrap();
632        loop {
633            let read = bufreader.read_line(&mut buffer).unwrap();
634            script_bytes += read;
635            if script_size != 0 && script_size > i {
636                script += &buffer;
637            } else if script_size != 0 && script_size <= i && filesize != 0 {
638                break;
639            }
640            if script_size == 0 {
641                if let Some(captures) = offset_reg.captures(&buffer) {
642                    if captures.len() > 1 {
643                        script_size = captures[1].to_string().parse().unwrap();
644                    }
645                }
646            }
647            if filesize == 0 {
648                if let Some(captures) = filesize_reg.captures(&buffer) {
649                    if captures.len() > 1 {
650                        filesize = captures[1].to_string().parse().unwrap();
651                    }
652                }
653            }
654            i += 1;
655        }
656        Ok((script_bytes, filesize))
657    }
658
659    // Downloads a file partially, using only access token instead of the full Gog struct
660    pub fn download_request_range_at<H: Handler>(
661        at: impl Into<String>,
662        url: impl Into<String>,
663        handler: H,
664        start: i64,
665        end: i64,
666    ) -> Result<Easy2<H>> {
667        let url = url.into();
668        let mut easy = Easy2::new(handler);
669        easy.url(&url)?;
670        easy.range(&format!("{}-{}", start, end))?;
671        easy.follow_location(true)?;
672        let mut list = curl::easy::List::new();
673        list.append("CSRF: true")?;
674        list.append(&format!("Authentication: Bearer {}", at.into()))?;
675        easy.get(true)?;
676        easy.http_headers(list)?;
677        easy.perform()?;
678        Ok(easy)
679    }
680
681    // Downloads a file partially
682    pub fn download_request_range(
683        &self,
684        url: impl Into<String>,
685        start: i64,
686        end: i64,
687    ) -> Result<Vec<u8>> {
688        Ok(Gog::download_request_range_at(
689            self.token.borrow().access_token.as_str(),
690            url,
691            Collector(Vec::new()),
692            start,
693            end,
694        )?
695        .get_ref()
696        .0
697        .clone())
698    }
699
700    // Extracts data on downloads
701    pub fn extract_data(&self, downloads: Vec<Download>) -> Result<Vec<ZipData>> {
702        let mut zips = vec![];
703        let mut responses = self.download_game(downloads.clone());
704        for down in downloads {
705            let mut url = BASE.to_string() + &down.manual_url;
706            let mut response;
707            loop {
708                if let Ok(temp_response) = self.client_noredirect.borrow().get(&url).send() {
709                    response = temp_response;
710                    let headers = response.headers();
711                    // GOG appears to be inconsistent with returning either 301/302,
712                    // so this just checks for a redirect location.
713                    if headers.contains_key("location") {
714                        url = headers
715                            .get("location")
716                            .unwrap()
717                            .to_str()
718                            .unwrap()
719                            .to_string();
720                    } else {
721                        break;
722                    }
723                }
724            }
725            let response = responses.remove(0).expect("Couldn't get download");
726            let size = response
727                .headers()
728                .get(CONTENT_LENGTH)
729                .unwrap()
730                .to_str()
731                .expect("Couldn't convert to string")
732                .parse()
733                .unwrap();
734            let mut bufreader = BufReader::new(response);
735            let sizes = self.get_sizes(&mut bufreader)?;
736            let eocd_offset = self.get_eocd_offset(&url, size)?;
737            let off = match eocd_offset {
738                EOCDOffset::Offset(offset) => offset,
739                EOCDOffset::Offset64(offset) => offset,
740            };
741            let cd_offset;
742            let records;
743            let cd_size;
744            let central_directory = self.download_request_range(url.as_str(), off as i64, size)?;
745            let mut cd_slice = central_directory.as_slice();
746            let mut cd_reader = BufReader::new(&mut cd_slice);
747            match eocd_offset {
748                EOCDOffset::Offset(..) => {
749                    let cd = CentralDirectory::from_reader(&mut cd_reader);
750                    cd_offset = cd.cd_start_offset as u64;
751                    records = cd.total_cd_records as u64;
752                    cd_size = cd.cd_size as u64;
753                }
754                EOCDOffset::Offset64(..) => {
755                    let cd = CentralDirectory64::from_reader(&mut cd_reader);
756                    cd_offset = cd.cd_start as u64;
757                    records = cd.cd_total;
758                    cd_size = cd.cd_size;
759                }
760            };
761            let offset_beg = sizes.0 + sizes.1 + cd_offset as usize;
762            let cd = self
763                .download_request_range(
764                    url.as_str(),
765                    offset_beg as i64,
766                    (offset_beg + cd_size as usize) as i64,
767                )
768                .unwrap();
769            let mut slice = cd.as_slice();
770            let mut full_reader = BufReader::new(&mut slice);
771            let mut files = vec![];
772            for _ in 0..records {
773                let mut entry = CDEntry::from_reader(&mut full_reader);
774                entry.start_offset = (sizes.0 + sizes.1) as u64 + entry.disk_offset.unwrap();
775                files.push(entry);
776            }
777            let len = files.len();
778            files[len - 1].end_offset = offset_beg as u64 - 1;
779            for i in 0..(len - 1) {
780                files[i].end_offset = files[i + 1].start_offset;
781            }
782            zips.push(ZipData {
783                sizes,
784                files,
785                url,
786                cd: None,
787            });
788        }
789        Ok(zips)
790    }
791
792    // Gets the EOCD offset from an url
793    fn get_eocd_offset(&self, url: &str, size: i64) -> Result<EOCDOffset> {
794        let signature = 0x06054b50;
795        let signature_64 = 0x06064b50;
796        let mut offset;
797        for i in 4..size + 1 {
798            let pos = size - i;
799            let resp = self.download_request_range(url, pos, pos + 4)?;
800            let cur = pos + 4;
801            let inte = vec_to_u32(&resp);
802            if inte == signature {
803                offset = cur;
804                offset -= 4;
805                return Ok(EOCDOffset::Offset(offset as usize));
806            } else if inte == signature_64 {
807                offset = cur;
808                offset -= 4;
809                return Ok(EOCDOffset::Offset64(offset as usize));
810            }
811        }
812        Err(NotAvailable.into())
813    }
814}
815
816fn fold_mult(acc: String, now: &String) -> String {
817    acc + "," + now
818}
819
820fn vec_to_u32(data: &[u8]) -> u32 {
821    u32::from_le_bytes([data[0], data[1], data[2], data[3]])
822}
823
824// A simple curl handler for a vector of bytes
825pub struct Collector(pub Vec<u8>);
826
827impl Handler for Collector {
828    fn write(&mut self, data: &[u8]) -> std::result::Result<usize, WriteError> {
829        self.0.extend_from_slice(data);
830        Ok(data.len())
831    }
832}