Skip to content

Commit 5f4f6e9

Browse files
authored
Add library validation step when installing from zip or git (#1234)
* Add library validation when installing from zip or git * [skip changelog] Fix tests temp directory deletion on Windows * [skip changelog] Fix test_install_git_invalid_library test * [skip changelog] Add more check in lib install git repo tests
1 parent ab298a3 commit 5f4f6e9

File tree

6 files changed

+159
-10
lines changed

6 files changed

+159
-10
lines changed

Diff for: 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+
}

Diff for: test/conftest.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ def data_dir(tmpdir_factory):
5656
if platform.system() == "Windows":
5757
with tempfile.TemporaryDirectory() as tmp:
5858
yield tmp
59-
shutil.rmtree(tmp, ignore_errors=True)
59+
# We don't need to remove the directory since
60+
# tempfile.TemporaryDirectory deletes itself
61+
# automatically when exits its scope.
6062
else:
6163
data = tmpdir_factory.mktemp("ArduinoTest")
6264
yield str(data)

Diff for: test/test_lib.py

+124-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,81 @@ 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+
with Repo.init(repo_dir) as repo:
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+
lib_install_dir = Path(data_dir, "libraries", "lib-without-header")
961+
# Verifies library is not already installed
962+
assert not lib_install_dir.exists()
963+
964+
res = run_command(f"lib install --git-url {repo_dir}", custom_env=env)
965+
assert res.failed
966+
assert 'library is not valid: missing header file "lib-without-header.h"' in res.stderr
967+
assert not lib_install_dir.exists()
968+
969+
# Create another fake library repository
970+
repo_dir = Path(data_dir, "lib-without-properties")
971+
with Repo.init(repo_dir) as repo:
972+
lib_header = Path(repo_dir, "src", "lib-without-properties.h")
973+
lib_header.parent.mkdir(parents=True, exist_ok=True)
974+
lib_header.touch()
975+
repo.index.add([str(lib_header)])
976+
repo.index.commit("First commit")
977+
978+
lib_install_dir = Path(data_dir, "libraries", "lib-without-properties")
979+
# Verifies library is not already installed
980+
assert not lib_install_dir.exists()
981+
982+
res = run_command(f"lib install --git-url {repo_dir}", custom_env=env)
983+
assert res.failed
984+
assert 'library is not valid: missing file "library.properties"' in res.stderr
985+
assert not lib_install_dir.exists()

Diff for: test/testdata/fake-lib.zip

174 Bytes
Binary file not shown.

Diff for: test/testdata/lib-without-header.zip

224 Bytes
Binary file not shown.

Diff for: test/testdata/lib-without-properties.zip

434 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)