77# See https://aboutcode.org for more information about nexB OSS projects.
88#
99
10+ import hashlib
11+ import json
1012import logging
1113import re
1214
1315import attr
16+ import saneyaml
1417
1518from commoncode import filetype
1619from packagedcode import models
20+ from packagedcode .licensing import get_license_matches
21+ from packagedcode .licensing import get_license_expression_from_matches
1722from packagedcode .spec import Spec
1823
24+ from packageurl import PackageURL
25+
1926"""
2027Handle cocoapods packages manifests for macOS and iOS
21- including .podspec, Podfile and Podfile.lock files.
28+ and from Xcode projects, including .podspec, Podfile and Podfile.lock files,
29+ and .podspec.json files from https://github.com/CocoaPods/Specs.
2230See https://cocoapods.org
2331"""
2432
3644
3745@attr .s ()
3846class CocoapodsPackage (models .Package ):
39- metafiles = ('*.podspec' ,)
40- extensions = ('.podspec' ,)
47+ metafiles = (
48+ '*.podspec' ,
49+ '*podfile.lock' ,
50+ '*.podspec.json' ,
51+ )
52+ extensions = (
53+ '.podspec' ,
54+ '.lock' ,
55+ )
4156 default_type = 'pods'
4257 default_primary_language = 'Objective-C'
4358 default_web_baseurl = 'https://cocoapods.org'
44- default_download_baseurl = None
45- default_api_baseurl = None
59+ github_specs_repo_baseurl = 'https://raw.githubusercontent.com/CocoaPods/Specs/blob/master/Specs'
60+ default_cdn_baseurl = 'https://cdn.cocoapods.org/Specs'
4661
4762 @classmethod
4863 def recognize (cls , location ):
4964 yield parse (location )
5065
5166 def repository_homepage_url (self , baseurl = default_web_baseurl ):
52- return '{ }/pods/{}' . format ( baseurl , self .name )
67+ return f' { baseurl } /pods/{ self .name } '
5368
5469 def repository_download_url (self ):
55- return '{}/archive/{}.zip' .format (self .homepage_url , self .version )
70+ if self .homepage_url :
71+ return f'{ self .homepage_url } /archive/{ self .version } .zip'
72+ elif self .reponame :
73+ return f'{ self .reponame } /archive/refs/tags/{ self .version } .zip'
74+
75+ def get_api_data_url (self ):
76+ return self .specs_json_github_url
77+
78+ def get_code_view_url (self ):
79+ if isinstance (self .reponame , str ):
80+ return self .reponame + '/tree/' + self .version
81+
82+ def get_bug_tracking_url (self ):
83+ if isinstance (self .reponame , str ):
84+ return self .reponame + '/issues/'
85+
86+ def specs_json_cdn_url (self , baseurl = default_cdn_baseurl ):
87+ return f'{ baseurl } /{ self .hashed_path } /{ self .name } /{ self .version } /{ self .name } .podspec.json'
88+
89+ def specs_json_github_url (self , baseurl = github_specs_repo_baseurl ):
90+ return f'{ baseurl } /{ self .hashed_path } /{ self .name } /{ self .version } /{ self .name } .podspec.json'
91+
92+ @property
93+ def reponame (self ):
94+ if isinstance (self .vcs_url , str ):
95+ if self .vcs_url [- 4 :] == '.git' :
96+ return self .vcs_url [:- 4 ]
97+
98+ @property
99+ def hashed_path (self ):
100+ """
101+ Returns a string with a part of the file path derived from the md5 hash.
102+
103+ From https://github.com/CocoaPods/cdn.cocoapods.org:
104+ "There are a set of known prefixes for all Podspec paths, you take the name of the pod,
105+ create a SHA (using md5) of it and take the first three characters."
106+ """
107+ podname = self .get_podname_proper (self .name )
108+ if self .name != podname :
109+ name_to_hash = podname
110+ else :
111+ name_to_hash = self .name
112+
113+ hash_init = self .get_first_3_mdf_hash_characters (name_to_hash )
114+ hashed_path = '/' .join (list (hash_init ))
115+ return hashed_path
116+
117+ @staticmethod
118+ def get_first_3_mdf_hash_characters (podname ):
119+ return hashlib .md5 (podname .encode ('utf-8' )).hexdigest ()[0 :3 ]
120+
121+ @staticmethod
122+ def get_podname_proper (podname ):
123+ """
124+ Podnames in cocoapods sometimes are files inside a pods package (like 'OHHTTPStubs/Default')
125+ This returns proper podname in those cases.
126+ """
127+ if '/' in podname :
128+ return podname .split ('/' )[0 ]
129+ return podname
56130
57131
58132def is_podspec (location ):
@@ -62,21 +136,64 @@ def is_podspec(location):
62136 return (filetype .is_file (location ) and location .endswith ('.podspec' ))
63137
64138
139+ def is_podfile_lock (location ):
140+ """
141+ Checks if the file is actually a podfile.lock file
142+ """
143+ return (filetype .is_file (location ) and location .endswith (('podfile.lock' , 'Podfile.lock' )))
144+
145+ def is_podspec_json (location ):
146+ """
147+ Checks if the file is actually a podspec.json metadata file
148+ """
149+ return (filetype .is_file (location ) and location .endswith ('.podspec.json' ))
150+
151+
152+ def read_podspec_json (location ):
153+ """
154+ Reads from podspec.json file at location as JSON.
155+ """
156+ with open (location , "r" ) as file :
157+ data = json .load (file )
158+
159+ return data
160+
161+
162+ def read_podfile_lock (location ):
163+ """
164+ Reads from podfile.lock file at location as YML.
165+ """
166+ with open (location , 'r' ) as file :
167+ data = saneyaml .load (file )
168+
169+ return data
170+
171+
65172def parse (location ):
66173 """
67- Return a Package object from a .podspec file or None.
174+ Return a Package object from:
175+ 1. `.podspec` files
176+ 2. `.podspec.json` files
177+ 3. `podfile.lock` files
178+ or returns None otherwise.
68179 """
69- if not is_podspec (location ):
70- return
180+ if is_podspec (location ):
181+ podspec_object = Spec ()
182+ podspec_data = podspec_object .parse_spec (location )
183+ return build_package (podspec_data )
184+
185+ if is_podspec_json (location ):
186+ podspec_json_data = read_podspec_json (location )
187+ return build_xcode_package (podspec_json_data )
71188
72- podspec_object = Spec ()
73- podspec_data = podspec_object . parse_spec (location )
74- return build_package ( podspec_data )
189+ if is_podfile_lock ( location ):
190+ podfile_lock_data = read_podfile_lock (location )
191+ return build_xcode_package_from_lockfile ( podfile_lock_data )
75192
76193
77194def build_package (podspec_data ):
78195 """
79- Return a Package object from a package data mapping or None .
196+ Return a Package object from a podspec.json package data mapping.
80197 """
81198 name = podspec_data .get ('name' )
82199 version = podspec_data .get ('version' )
@@ -170,3 +287,173 @@ def parse_person(person):
170287 email = parsed .group ('email' )
171288
172289 return name , email
290+
291+
292+ def get_sha1_file (location ):
293+ """
294+ Get sha1 hash for a file at location.
295+ """
296+ with open (location , "rb" ) as f :
297+ return hashlib .sha1 (f .read ()).hexdigest ()
298+
299+
300+ def build_xcode_package (podspec_json_data ):
301+ """
302+ Return a Package object from a podspec.json package data mapping.
303+ """
304+ name = podspec_json_data .get ('name' )
305+ version = podspec_json_data .get ('version' )
306+ summary = podspec_json_data .get ('summary' , '' )
307+ description = podspec_json_data .get ('description' , '' )
308+ homepage_url = podspec_json_data .get ('homepage' )
309+
310+ license = podspec_json_data .get ('license' )
311+ if isinstance (license , dict ):
312+ declared_license = ' ' .join (list (license .values ()))
313+ else :
314+ declared_license = license
315+
316+ source = podspec_json_data .get ('source' )
317+ vcs_url = None
318+ download_url = None
319+
320+ if isinstance (source , dict ):
321+ git_url = source .get ('git' , '' )
322+ http_url = source .get ('http' , '' )
323+ if git_url :
324+ vcs_url = git_url
325+ elif http_url :
326+ download_url = http_url
327+
328+ if not vcs_url :
329+ vcs_url = source
330+
331+ authors = podspec_json_data .get ('authors' ) or {}
332+
333+ license_matches = get_license_matches (query_string = declared_license )
334+ if not license_matches :
335+ license_expression = 'unknown'
336+ else :
337+ license_expression = get_license_expression_from_matches (license_matches )
338+
339+ if summary and not description .startswith (summary ):
340+ desc = [summary ]
341+ if description :
342+ desc += [description ]
343+ description = '. ' .join (desc )
344+
345+ parties = []
346+ if authors :
347+ if isinstance (authors , dict ):
348+ for key , value in authors .items ():
349+ party = models .Party (
350+ type = models .party_org ,
351+ name = key ,
352+ url = value + '.com' ,
353+ role = 'owner'
354+ )
355+ parties .append (party )
356+ else :
357+ party = models .Party (
358+ type = models .party_org ,
359+ name = authors ,
360+ role = 'owner'
361+ )
362+ parties .append (party )
363+
364+ extra_data = {}
365+ extra_data ['source' ] = podspec_json_data ['source' ]
366+ dependencies = podspec_json_data .get ('dependencies' , '' )
367+ if dependencies :
368+ extra_data ['dependencies' ] = dependencies
369+ extra_data ['podspec.json' ] = podspec_json_data
370+
371+ package = CocoapodsPackage (
372+ name = name ,
373+ version = version ,
374+ vcs_url = vcs_url ,
375+ description = description ,
376+ declared_license = declared_license ,
377+ license_expression = license_expression ,
378+ homepage_url = homepage_url ,
379+ download_url = download_url ,
380+ parties = parties ,
381+ )
382+
383+ package .api_data_url = package .get_api_data_url ()
384+
385+ return package
386+
387+
388+ def get_data_from_pods (dep_pod_version ):
389+
390+ if '(' in dep_pod_version :
391+ podname , _ , version = dep_pod_version .strip (')' ).partition (' (' )
392+ else :
393+ version = None
394+ podname = dep_pod_version
395+
396+ if '/' in podname :
397+ namespace , _ , podname = podname .partition ('/' )
398+ else :
399+ namespace = None
400+
401+ return podname , namespace , version
402+
403+
404+ def build_xcode_package_from_lockfile (podfile_lock_data ):
405+ """
406+ Return a Package object from a data mapping obtained from a podfile.lock
407+ """
408+ pods = podfile_lock_data ['PODS' ]
409+ pod_deps = []
410+
411+ for pod in pods :
412+
413+ if isinstance (pod , dict ):
414+ for main_pod , _dep_pods in pod .items ():
415+
416+ podname , namespace , version = get_data_from_pods (main_pod )
417+
418+ purl = PackageURL (
419+ type = 'pods' ,
420+ namespace = namespace ,
421+ name = podname ,
422+ version = version ,
423+ ).to_string ()
424+
425+ pod_deps .append (
426+ models .DependentPackage (
427+ purl = purl ,
428+ scope = 'requires-dev' ,
429+ requirement = version ,
430+ is_runtime = False ,
431+ is_optional = True ,
432+ is_resolved = True ,
433+ )
434+ )
435+
436+ elif isinstance (pod , str ):
437+ podname , namespace , version = get_data_from_pods (pod )
438+ purl = PackageURL (
439+ type = 'pods' ,
440+ namespace = namespace ,
441+ name = podname ,
442+ version = version ,
443+ ).to_string ()
444+
445+ pod_deps .append (
446+ models .DependentPackage (
447+ purl = purl ,
448+ scope = 'requires-dev' ,
449+ requirement = version ,
450+ is_runtime = False ,
451+ is_optional = True ,
452+ is_resolved = True ,
453+ )
454+ )
455+
456+ yield CocoapodsPackage (
457+ dependencies = pod_deps ,
458+ declared_license = None ,
459+ )
0 commit comments