Skip to content

Commit a49e375

Browse files
committed
Add library validation when installing from zip or git
1 parent 6015f4e commit a49e375

File tree

5 files changed

+146
-9
lines changed

5 files changed

+146
-9
lines changed

arduino/libraries/librariesmanager/install.go

+32
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ func (lm *LibrariesManager) InstallZipLib(ctx context.Context, archivePath strin
133133

134134
extractionPath := paths[0]
135135
libraryName := extractionPath.Base()
136+
137+
if err := validateLibrary(libraryName, extractionPath); err != nil {
138+
return err
139+
}
140+
136141
installPath := libsDir.Join(libraryName)
137142

138143
if err := libsDir.MkdirAll(); err != nil {
@@ -218,6 +223,13 @@ func (lm *LibrariesManager) InstallGitLib(gitURL string, overwrite bool) error {
218223
Warn("Cloning git repository")
219224
return err
220225
}
226+
227+
if err := validateLibrary(libraryName, installPath); err != nil {
228+
// Clean up installation directory since this is not a valid library
229+
installPath.RemoveAll()
230+
return err
231+
}
232+
221233
// We don't want the installed library to be a git repository thus we delete this folder
222234
installPath.Join(".git").RemoveAll()
223235
return nil
@@ -239,3 +251,23 @@ func parseGitURL(gitURL string) (string, error) {
239251
}
240252
return res, nil
241253
}
254+
255+
// validateLibrary verifies the dir contains a valid library, meaning it has both
256+
// an header <name>.h, either in src or root folder, and a library.properties file
257+
func validateLibrary(name string, dir *paths.Path) error {
258+
// Verify library contains library header.
259+
// Checks also root folder for legacy reasons.
260+
// For more info see:
261+
// https://arduino.github.io/arduino-cli/latest/library-specification/#source-code
262+
libraryHeader := name + ".h"
263+
if !dir.Join("src", libraryHeader).Exist() && !dir.Join(libraryHeader).Exist() {
264+
return fmt.Errorf(`library is not valid: missing header file "%s"`, libraryHeader)
265+
}
266+
267+
// Verifies library contains library.properties
268+
if !dir.Join("library.properties").Exist() {
269+
return fmt.Errorf(`library is not valid: missing file "library.properties"`)
270+
}
271+
272+
return nil
273+
}

test/test_lib.py

+114-9
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,34 @@
1818
import pytest
1919
from git import Repo
2020
from pathlib import Path
21+
import tempfile
22+
import requests
23+
import zipfile
24+
import io
25+
import re
26+
27+
28+
# Util function to download library from URL
29+
def download_lib(url, download_dir):
30+
tmp = Path(tempfile.TemporaryDirectory().name)
31+
tmp.mkdir(parents=True, exist_ok=True)
32+
regex = re.compile(r"^(.*)-[0-9]+.[0-9]+.[0-9]")
33+
response = requests.get(url)
34+
# Download and unzips library removing version suffix
35+
with zipfile.ZipFile(io.BytesIO(response.content)) as thezip:
36+
for zipinfo in thezip.infolist():
37+
with thezip.open(zipinfo) as f:
38+
dest_dir = tmp / regex.sub("\\g<1>", zipinfo.filename)
39+
if zipinfo.is_dir():
40+
dest_dir.mkdir(parents=True, exist_ok=True)
41+
else:
42+
dest_dir.write_bytes(f.read())
43+
44+
# Recreates zip with folder without version suffix
45+
z = zipfile.ZipFile(download_dir, "w")
46+
for f in tmp.glob("**/*"):
47+
z.write(f, arcname=f.relative_to(tmp))
48+
z.close()
2149

2250

2351
def test_list(run_command):
@@ -252,8 +280,12 @@ def test_install_git_url_and_zip_path_flags_visibility(run_command, data_dir, do
252280
assert res.failed
253281
assert "--git-url and --zip-path are disabled by default, for more information see:" in res.stderr
254282

255-
assert run_command("lib download [email protected]")
256-
zip_path = Path(downloads_dir, "libraries", "AudioZero-1.0.0.zip")
283+
# Download library
284+
url = "https://github.com/arduino-libraries/AudioZero/archive/refs/tags/1.1.1.zip"
285+
zip_path = Path(downloads_dir, "libraries", "AudioZero.zip")
286+
zip_path.parent.mkdir(parents=True, exist_ok=True)
287+
download_lib(url, zip_path)
288+
257289
res = run_command(f"lib install --zip-path {zip_path}")
258290
assert res.failed
259291
assert "--git-url and --zip-path are disabled by default, for more information see:" in res.stderr
@@ -330,13 +362,16 @@ def test_install_with_zip_path(run_command, data_dir, downloads_dir):
330362
assert run_command("config init --dest-dir .", custom_env=env)
331363

332364
# Download a specific lib version
333-
assert run_command("lib download [email protected]")
365+
# Download library
366+
url = "https://github.com/arduino-libraries/AudioZero/archive/refs/tags/1.1.1.zip"
367+
zip_path = Path(downloads_dir, "libraries", "AudioZero.zip")
368+
zip_path.parent.mkdir(parents=True, exist_ok=True)
369+
download_lib(url, zip_path)
334370

335-
lib_install_dir = Path(data_dir, "libraries", "AudioZero-1.0.0")
371+
lib_install_dir = Path(data_dir, "libraries", "AudioZero")
336372
# Verifies library is not already installed
337373
assert not lib_install_dir.exists()
338374

339-
zip_path = Path(downloads_dir, "libraries", "AudioZero-1.0.0.zip")
340375
# Test zip-path install
341376
res = run_command(f"lib install --zip-path {zip_path}")
342377
assert res.ok
@@ -688,13 +723,14 @@ def test_install_with_zip_path_multiple_libraries(run_command, downloads_dir, da
688723
}
689724

690725
# Downloads zip to be installed later
691-
assert run_command("lib download [email protected]")
692-
assert run_command("lib download [email protected]")
693726
wifi_zip_path = Path(downloads_dir, "libraries", "WiFi101-0.16.1.zip")
694727
ble_zip_path = Path(downloads_dir, "libraries", "ArduinoBLE-1.1.3.zip")
728+
download_lib("https://github.com/arduino-libraries/WiFi101/archive/refs/tags/0.16.1.zip", wifi_zip_path)
729+
download_lib("https://github.com/arduino-libraries/ArduinoBLE/archive/refs/tags/1.1.3.zip", ble_zip_path)
730+
731+
wifi_install_dir = Path(data_dir, "libraries", "WiFi101")
732+
ble_install_dir = Path(data_dir, "libraries", "ArduinoBLE")
695733

696-
wifi_install_dir = Path(data_dir, "libraries", "WiFi101-0.16.1")
697-
ble_install_dir = Path(data_dir, "libraries", "ArduinoBLE-1.1.3")
698734
# Verifies libraries are not installed
699735
assert not wifi_install_dir.exists()
700736
assert not ble_install_dir.exists()
@@ -860,6 +896,7 @@ def test_install_zip_lib_with_macos_metadata(run_command, data_dir, downloads_di
860896
assert lib_install_dir.exists()
861897
files = list(lib_install_dir.glob("**/*"))
862898
assert lib_install_dir / "library.properties" in files
899+
assert lib_install_dir / "src" / "fake-lib.h" in files
863900

864901
# Reinstall library
865902
assert run_command(f"lib install --zip-path {zip_path}")
@@ -868,3 +905,71 @@ def test_install_zip_lib_with_macos_metadata(run_command, data_dir, downloads_di
868905
assert lib_install_dir.exists()
869906
files = list(lib_install_dir.glob("**/*"))
870907
assert lib_install_dir / "library.properties" in files
908+
assert lib_install_dir / "src" / "fake-lib.h" in files
909+
910+
911+
def test_install_zip_invalid_library(run_command, data_dir, downloads_dir):
912+
# Initialize configs to enable --zip-path flag
913+
env = {
914+
"ARDUINO_DATA_DIR": data_dir,
915+
"ARDUINO_DOWNLOADS_DIR": downloads_dir,
916+
"ARDUINO_SKETCHBOOK_DIR": data_dir,
917+
"ARDUINO_ENABLE_UNSAFE_LIBRARY_INSTALL": "true",
918+
}
919+
assert run_command("config init --dest-dir .", custom_env=env)
920+
921+
lib_install_dir = Path(data_dir, "libraries", "lib-without-header")
922+
# Verifies library is not already installed
923+
assert not lib_install_dir.exists()
924+
925+
zip_path = Path(__file__).parent / "testdata" / "lib-without-header.zip"
926+
# Test zip-path install
927+
res = run_command(f"lib install --zip-path {zip_path}")
928+
assert res.failed
929+
assert 'library is not valid: missing header file "lib-without-header.h"' in res.stderr
930+
931+
lib_install_dir = Path(data_dir, "libraries", "lib-without-properties")
932+
# Verifies library is not already installed
933+
assert not lib_install_dir.exists()
934+
935+
zip_path = Path(__file__).parent / "testdata" / "lib-without-properties.zip"
936+
# Test zip-path install
937+
res = run_command(f"lib install --zip-path {zip_path}")
938+
assert res.failed
939+
assert 'library is not valid: missing file "library.properties"' in res.stderr
940+
941+
942+
def test_install_git_invalid_library(run_command, data_dir, downloads_dir):
943+
# Initialize configs to enable --zip-path flag
944+
env = {
945+
"ARDUINO_DATA_DIR": data_dir,
946+
"ARDUINO_DOWNLOADS_DIR": downloads_dir,
947+
"ARDUINO_SKETCHBOOK_DIR": data_dir,
948+
"ARDUINO_ENABLE_UNSAFE_LIBRARY_INSTALL": "true",
949+
}
950+
assert run_command("config init --dest-dir .", custom_env=env)
951+
952+
# Create fake library repository
953+
repo_dir = Path(data_dir, "lib-without-header")
954+
repo = Repo.init(repo_dir)
955+
lib_properties = Path(repo_dir, "library.properties")
956+
lib_properties.touch()
957+
repo.index.add([str(lib_properties)])
958+
repo.index.commit("First commit")
959+
960+
res = run_command(f"lib install --git-url {repo_dir}", custom_env=env)
961+
assert res.failed
962+
assert 'library is not valid: missing header file "lib-without-header.h"' in res.stderr
963+
964+
# Create another fake library repository
965+
repo_dir = Path(data_dir, "lib-without-properties")
966+
repo = Repo.init(repo_dir)
967+
lib_header = Path(repo_dir, "src", "lib-without-properties.h")
968+
lib_header.parent.mkdir(parents=True, exist_ok=True)
969+
lib_header.touch()
970+
repo.index.add([str(lib_header)])
971+
repo.index.commit("First commit")
972+
973+
res = run_command(f"lib install --git-url {repo_dir}", custom_env=env)
974+
assert res.failed
975+
assert 'library is not valid: missing file "library.properties"' in res.stderr

test/testdata/fake-lib.zip

174 Bytes
Binary file not shown.

test/testdata/lib-without-header.zip

224 Bytes
Binary file not shown.
434 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)