mirror of
				https://github.com/pbatard/rufus.git
				synced 2024-08-14 23:57:05 +00:00 
			
		
		
		
	[syslinux] complete Syslinux v5 support
* Add download and copy of mandatory ldlinux.c32 to target * With previous patch, this should address all Syslinux v5 related issues * Closes #142
This commit is contained in:
		
							parent
							
								
									df5bce297d
								
							
						
					
					
						commit
						1dbaff6897
					
				
					 4 changed files with 90 additions and 38 deletions
				
			
		
							
								
								
									
										99
									
								
								src/rufus.c
									
										
									
									
									
								
							
							
						
						
									
										99
									
								
								src/rufus.c
									
										
									
									
									
								
							|  | @ -98,6 +98,7 @@ static BOOL log_displayed = FALSE; | ||||||
| static BOOL iso_provided = FALSE; | static BOOL iso_provided = FALSE; | ||||||
| extern BOOL force_large_fat32; | extern BOOL force_large_fat32; | ||||||
| static int selection_default; | static int selection_default; | ||||||
|  | char msgbox[1024], msgbox_title[32]; | ||||||
| 
 | 
 | ||||||
| /*
 | /*
 | ||||||
|  * Globals |  * Globals | ||||||
|  | @ -947,7 +948,7 @@ static void CALLBACK BlockingTimer(HWND hWnd, UINT uMsg, UINT_PTR idEvent, DWORD | ||||||
| 		user_notified = TRUE; | 		user_notified = TRUE; | ||||||
| 		uprintf("Blocking I/O operation detected\n"); | 		uprintf("Blocking I/O operation detected\n"); | ||||||
| 		MessageBoxU(hMainDialog, | 		MessageBoxU(hMainDialog, | ||||||
| 			"Rufus detected that Windows is still flushing its internal buffers\n" | 			APPLICATION_NAME " detected that Windows is still flushing its internal buffers\n" | ||||||
| 			"onto the USB device.\n\n" | 			"onto the USB device.\n\n" | ||||||
| 			"Depending on the speed of your USB device, this operation may\n" | 			"Depending on the speed of your USB device, this operation may\n" | ||||||
| 			"take a long time to complete, especially for large files.\n\n" | 			"take a long time to complete, especially for large files.\n\n" | ||||||
|  | @ -1010,7 +1011,6 @@ DWORD WINAPI ISOScanThread(LPVOID param) | ||||||
| 	FILE* fd; | 	FILE* fd; | ||||||
| 	const char* old_c32_name[NB_OLD_C32] = OLD_C32_NAMES; | 	const char* old_c32_name[NB_OLD_C32] = OLD_C32_NAMES; | ||||||
| 	const char* new_c32_url[NB_OLD_C32] = NEW_C32_URL; | 	const char* new_c32_url[NB_OLD_C32] = NEW_C32_URL; | ||||||
| 	char msg[1024], msg_title[32]; |  | ||||||
| 
 | 
 | ||||||
| 	if (iso_path == NULL) | 	if (iso_path == NULL) | ||||||
| 		goto out; | 		goto out; | ||||||
|  | @ -1032,7 +1032,7 @@ DWORD WINAPI ISOScanThread(LPVOID param) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	if ((!iso_report.has_bootmgr) && (!iso_report.has_isolinux) && (!IS_WINPE(iso_report.winpe)) && (!iso_report.has_efi)) { | 	if ((!iso_report.has_bootmgr) && (!iso_report.has_isolinux) && (!IS_WINPE(iso_report.winpe)) && (!iso_report.has_efi)) { | ||||||
| 		MessageBoxU(hMainDialog, "This version of Rufus only supports bootable ISOs\n" | 		MessageBoxU(hMainDialog, "This version of " APPLICATION_NAME " only supports bootable ISOs\n" | ||||||
| 			"based on bootmgr/WinPE, isolinux or EFI.\n" | 			"based on bootmgr/WinPE, isolinux or EFI.\n" | ||||||
| 			"This ISO doesn't appear to use either...", "Unsupported ISO", MB_OK|MB_ICONINFORMATION); | 			"This ISO doesn't appear to use either...", "Unsupported ISO", MB_OK|MB_ICONINFORMATION); | ||||||
| 		safe_free(iso_path); | 		safe_free(iso_path); | ||||||
|  | @ -1053,16 +1053,16 @@ DWORD WINAPI ISOScanThread(LPVOID param) | ||||||
| 					use_own_c32[i] = TRUE; | 					use_own_c32[i] = TRUE; | ||||||
| 				} else { | 				} else { | ||||||
| 					PrintStatus(0, FALSE, "Obsolete %s detected", old_c32_name[i]); | 					PrintStatus(0, FALSE, "Obsolete %s detected", old_c32_name[i]); | ||||||
| 					safe_sprintf(msg, sizeof(msg), "This ISO image seems to use an obsolete version of '%s'.\n" | 					safe_sprintf(msgbox, sizeof(msgbox), "This ISO image seems to use an obsolete version of '%s'.\n" | ||||||
| 						"Because of this, boot menus may not display properly.\n\n" | 						"Because of this, boot menus may not display properly.\n\n" | ||||||
| 						"Rufus can fix this issue by downloading a newer version for you:\n" | 						APPLICATION_NAME " can fix this issue by downloading a newer version for you:\n" | ||||||
| 						"- Choose 'Yes' to connect to the internet and replace the file.\n" | 						"- Choose 'Yes' to connect to the internet and replace the file\n" | ||||||
| 						"- Choose 'No' to leave the existing ISO file unmodified.\n" | 						"- Choose 'No' to leave the existing ISO file unmodified\n" | ||||||
| 						"If you don't know what to do, you should select 'Yes'.\n\n" | 						"If you don't know what to do, you should select 'Yes'.\n\n" | ||||||
| 						"Note: the new file will be downloaded in the current directory and once a " | 						"Note: The new file will be downloaded in the current directory and once a " | ||||||
| 						"'%s' exists there, it will always be used as replacement.\n", old_c32_name[i], old_c32_name[i]); | 						"'%s' exists there, it will be reused automatically.\n", old_c32_name[i], old_c32_name[i]); | ||||||
| 					safe_sprintf(msg_title, sizeof(msg_title), "Replace %s?", old_c32_name[i]); | 					safe_sprintf(msgbox_title, sizeof(msgbox_title), "Replace %s?", old_c32_name[i]); | ||||||
| 					if (MessageBoxA(hMainDialog, msg, msg_title, MB_YESNO|MB_ICONWARNING) == IDYES) { | 					if (MessageBoxA(hMainDialog, msgbox, msgbox_title, MB_YESNO|MB_ICONWARNING) == IDYES) { | ||||||
| 						SetWindowTextU(hISOProgressDlg, "Downloading file..."); | 						SetWindowTextU(hISOProgressDlg, "Downloading file..."); | ||||||
| 						SetWindowTextU(hISOFileName, new_c32_url[i]); | 						SetWindowTextU(hISOFileName, new_c32_url[i]); | ||||||
| 						if (DownloadFile(new_c32_url[i], old_c32_name[i], hISOProgressDlg)) | 						if (DownloadFile(new_c32_url[i], old_c32_name[i], hISOProgressDlg)) | ||||||
|  | @ -1176,9 +1176,12 @@ void ToggleAdvanced(void) | ||||||
| 
 | 
 | ||||||
| static BOOL BootCheck(void) | static BOOL BootCheck(void) | ||||||
| { | { | ||||||
| 	int fs, bt; | 	int fs, bt, dt, r; | ||||||
|  | 	FILE* fd; | ||||||
|  | 	const char* ldlinux_c32 = "ldlinux.c32"; | ||||||
| 
 | 
 | ||||||
| 	if (ComboBox_GetItemData(hBootType, ComboBox_GetCurSel(hBootType)) == DT_ISO) { | 	dt = (int)ComboBox_GetItemData(hBootType, ComboBox_GetCurSel(hBootType)); | ||||||
|  | 	if (dt == DT_ISO) { | ||||||
| 		if (iso_path == NULL) { | 		if (iso_path == NULL) { | ||||||
| 			MessageBoxA(hMainDialog, "Please click on the disc button to select a bootable ISO,\n" | 			MessageBoxA(hMainDialog, "Please click on the disc button to select a bootable ISO,\n" | ||||||
| 				"or uncheck the \"Create a bootable disk...\" checkbox.", | 				"or uncheck the \"Create a bootable disk...\" checkbox.", | ||||||
|  | @ -1224,6 +1227,31 @@ static BOOL BootCheck(void) | ||||||
| 				ShellExecuteA(hMainDialog, "open", SEVENZIP_URL, NULL, NULL, SW_SHOWNORMAL); | 				ShellExecuteA(hMainDialog, "open", SEVENZIP_URL, NULL, NULL, SW_SHOWNORMAL); | ||||||
| 			return FALSE; | 			return FALSE; | ||||||
| 		} | 		} | ||||||
|  | 	} else if (dt == DT_SYSLINUX_V5) { | ||||||
|  | 		fd = fopen(ldlinux_c32, "rb"); | ||||||
|  | 		if (fd != NULL) { | ||||||
|  | 			uprintf("Will reuse '%s' for Syslinux v5\n", ldlinux_c32); | ||||||
|  | 			fclose(fd); | ||||||
|  | 		} else { | ||||||
|  | 			PrintStatus(0, FALSE, "Missing '%s' file", ldlinux_c32); | ||||||
|  | 			safe_sprintf(msgbox, sizeof(msgbox), "Syslinux v5.0 or later requires a '%s' file to be installed.\n" | ||||||
|  | 				"Because this file is more than 100 KB in size, and always present on Syslinux v5+ ISO images, " | ||||||
|  | 				"it is not embedded in " APPLICATION_NAME ".\n\n" | ||||||
|  | 				APPLICATION_NAME " can download the missing file for you:\n" | ||||||
|  | 				"- Select 'Yes' to connect to the internet and download the file\n" | ||||||
|  | 				"- Select 'No' if you will manually copy this file on the drive later\n\n" | ||||||
|  | 				"Note: The file will be downloaded in the current directory and once a " | ||||||
|  | 				"'%s' exists there, it will be reused automatically.\n", ldlinux_c32, ldlinux_c32, ldlinux_c32); | ||||||
|  | 			safe_sprintf(msgbox_title, sizeof(msgbox_title), "Download %s?", ldlinux_c32); | ||||||
|  | 			r = MessageBoxA(hMainDialog, msgbox, msgbox_title, MB_YESNOCANCEL|MB_ICONWARNING); | ||||||
|  | 			if (r == IDCANCEL)  | ||||||
|  | 				return FALSE; | ||||||
|  | 			if (r == IDYES) { | ||||||
|  | 				SetWindowTextU(hISOProgressDlg, "Downloading file..."); | ||||||
|  | 				SetWindowTextU(hISOFileName, ldlinux_c32); | ||||||
|  | 				DownloadFile(LDLINUX_C32_URL, ldlinux_c32, hISOProgressDlg); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 	return TRUE; | 	return TRUE; | ||||||
| } | } | ||||||
|  | @ -1695,31 +1723,36 @@ static INT_PTR CALLBACK MainCallback(HWND hDlg, UINT message, WPARAM wParam, LPA | ||||||
| 			selection_default =  (int)ComboBox_GetItemData(hBootType, ComboBox_GetCurSel(hBootType)); | 			selection_default =  (int)ComboBox_GetItemData(hBootType, ComboBox_GetCurSel(hBootType)); | ||||||
| 			nDeviceIndex = ComboBox_GetCurSel(hDeviceList); | 			nDeviceIndex = ComboBox_GetCurSel(hDeviceList); | ||||||
| 			if (nDeviceIndex != CB_ERR) { | 			if (nDeviceIndex != CB_ERR) { | ||||||
| 				if ((IsChecked(IDC_BOOT)) && (!BootCheck())) | 				if ((IsChecked(IDC_BOOT)) && (!BootCheck())) { | ||||||
|  | 					format_op_in_progress = FALSE; | ||||||
| 					break; | 					break; | ||||||
|  | 				} | ||||||
| 				GetWindowTextU(hDeviceList, tmp, ARRAYSIZE(tmp)); | 				GetWindowTextU(hDeviceList, tmp, ARRAYSIZE(tmp)); | ||||||
| 				_snprintf(str, ARRAYSIZE(str), "WARNING: ALL DATA ON DEVICE '%s'\r\nWILL BE DESTROYED.\r\n" | 				_snprintf(str, ARRAYSIZE(str), "WARNING: ALL DATA ON DEVICE '%s'\r\nWILL BE DESTROYED.\r\n" | ||||||
| 					"To continue with this operation, click OK. To quit click CANCEL.", tmp); | 					"To continue with this operation, click OK. To quit click CANCEL.", tmp); | ||||||
| 				if (MessageBoxU(hMainDialog, str, APPLICATION_NAME, MB_OKCANCEL|MB_ICONWARNING) == IDOK) { | 				if (MessageBoxU(hMainDialog, str, APPLICATION_NAME, MB_OKCANCEL|MB_ICONWARNING) == IDCANCEL) { | ||||||
| 					// Disable all controls except cancel
 | 					format_op_in_progress = FALSE; | ||||||
| 					EnableControls(FALSE); | 					break; | ||||||
| 					DeviceNum = (DWORD)ComboBox_GetItemData(hDeviceList, nDeviceIndex); |  | ||||||
| 					FormatStatus = 0; |  | ||||||
| 					InitProgress(); |  | ||||||
| 					format_thid = CreateThread(NULL, 0, FormatThread, (LPVOID)(uintptr_t)DeviceNum, 0, NULL); |  | ||||||
| 					if (format_thid == NULL) { |  | ||||||
| 						uprintf("Unable to start formatting thread"); |  | ||||||
| 						FormatStatus = ERROR_SEVERITY_ERROR|FAC(FACILITY_STORAGE)|APPERR(ERROR_CANT_START_THREAD); |  | ||||||
| 						PostMessage(hMainDialog, UM_FORMAT_COMPLETED, 0, 0); |  | ||||||
| 					} |  | ||||||
| 					uprintf("\r\nFormat operation started"); |  | ||||||
| 					PrintStatus(0, FALSE, ""); |  | ||||||
| 					timer = 0; |  | ||||||
| 					safe_sprintf(szTimer, sizeof(szTimer), "00:00:00"); |  | ||||||
| 					SendMessageA(GetDlgItem(hMainDialog, IDC_STATUS), SB_SETTEXTA, |  | ||||||
| 						SBT_OWNERDRAW | 1, (LPARAM)szTimer); |  | ||||||
| 					SetTimer(hMainDialog, TID_APP_TIMER, 1000, ClockTimer); |  | ||||||
| 				} | 				} | ||||||
|  | 
 | ||||||
|  | 				// Disable all controls except cancel
 | ||||||
|  | 				EnableControls(FALSE); | ||||||
|  | 				DeviceNum = (DWORD)ComboBox_GetItemData(hDeviceList, nDeviceIndex); | ||||||
|  | 				FormatStatus = 0; | ||||||
|  | 				InitProgress(); | ||||||
|  | 				format_thid = CreateThread(NULL, 0, FormatThread, (LPVOID)(uintptr_t)DeviceNum, 0, NULL); | ||||||
|  | 				if (format_thid == NULL) { | ||||||
|  | 					uprintf("Unable to start formatting thread"); | ||||||
|  | 					FormatStatus = ERROR_SEVERITY_ERROR|FAC(FACILITY_STORAGE)|APPERR(ERROR_CANT_START_THREAD); | ||||||
|  | 					PostMessage(hMainDialog, UM_FORMAT_COMPLETED, 0, 0); | ||||||
|  | 				} | ||||||
|  | 				uprintf("\r\nFormat operation started"); | ||||||
|  | 				PrintStatus(0, FALSE, ""); | ||||||
|  | 				timer = 0; | ||||||
|  | 				safe_sprintf(szTimer, sizeof(szTimer), "00:00:00"); | ||||||
|  | 				SendMessageA(GetDlgItem(hMainDialog, IDC_STATUS), SB_SETTEXTA, | ||||||
|  | 					SBT_OWNERDRAW | 1, (LPARAM)szTimer); | ||||||
|  | 				SetTimer(hMainDialog, TID_APP_TIMER, 1000, ClockTimer); | ||||||
| 			} | 			} | ||||||
| 			if (format_thid == NULL) | 			if (format_thid == NULL) | ||||||
| 				format_op_in_progress = FALSE; | 				format_op_in_progress = FALSE; | ||||||
|  |  | ||||||
|  | @ -195,6 +195,7 @@ typedef struct { | ||||||
| #define OLD_C32_NAMES       {"menu.c32", "vesamenu.c32"} | #define OLD_C32_NAMES       {"menu.c32", "vesamenu.c32"} | ||||||
| #define OLD_C32_THRESHOLD   {53500, 148000} | #define OLD_C32_THRESHOLD   {53500, 148000} | ||||||
| #define NEW_C32_URL         {RUFUS_URL "/downloads/menu.c32", RUFUS_URL "/downloads/vesamenu.c32"} | #define NEW_C32_URL         {RUFUS_URL "/downloads/menu.c32", RUFUS_URL "/downloads/vesamenu.c32"} | ||||||
|  | #define LDLINUX_C32_URL     RUFUS_URL "/downloads/ldlinux.c32" | ||||||
| 
 | 
 | ||||||
| /* ISO details that the application may want */ | /* ISO details that the application may want */ | ||||||
| #define WINPE_MININT    0x2A | #define WINPE_MININT    0x2A | ||||||
|  |  | ||||||
							
								
								
									
										10
									
								
								src/rufus.rc
									
										
									
									
									
								
							
							
						
						
									
										10
									
								
								src/rufus.rc
									
										
									
									
									
								
							|  | @ -30,7 +30,7 @@ LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL | ||||||
| IDD_DIALOG DIALOGEX 12, 12, 206, 329 | IDD_DIALOG DIALOGEX 12, 12, 206, 329 | ||||||
| STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU | STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU | ||||||
| EXSTYLE WS_EX_APPWINDOW | EXSTYLE WS_EX_APPWINDOW | ||||||
| CAPTION "Rufus v1.3.4.253" | CAPTION "Rufus v1.3.4.254" | ||||||
| FONT 8, "MS Shell Dlg", 400, 0, 0x1 | FONT 8, "MS Shell Dlg", 400, 0, 0x1 | ||||||
| BEGIN | BEGIN | ||||||
|     DEFPUSHBUTTON   "Start",IDC_START,94,291,50,14 |     DEFPUSHBUTTON   "Start",IDC_START,94,291,50,14 | ||||||
|  | @ -278,8 +278,8 @@ END | ||||||
| // | // | ||||||
| 
 | 
 | ||||||
| VS_VERSION_INFO VERSIONINFO | VS_VERSION_INFO VERSIONINFO | ||||||
|  FILEVERSION 1,3,4,253 |  FILEVERSION 1,3,4,254 | ||||||
|  PRODUCTVERSION 1,3,4,253 |  PRODUCTVERSION 1,3,4,254 | ||||||
|  FILEFLAGSMASK 0x3fL |  FILEFLAGSMASK 0x3fL | ||||||
| #ifdef _DEBUG | #ifdef _DEBUG | ||||||
|  FILEFLAGS 0x1L |  FILEFLAGS 0x1L | ||||||
|  | @ -296,13 +296,13 @@ BEGIN | ||||||
|         BEGIN |         BEGIN | ||||||
|             VALUE "CompanyName", "Akeo Consulting (http://akeo.ie)" |             VALUE "CompanyName", "Akeo Consulting (http://akeo.ie)" | ||||||
|             VALUE "FileDescription", "Rufus" |             VALUE "FileDescription", "Rufus" | ||||||
|             VALUE "FileVersion", "1.3.4.253" |             VALUE "FileVersion", "1.3.4.254" | ||||||
|             VALUE "InternalName", "Rufus" |             VALUE "InternalName", "Rufus" | ||||||
|             VALUE "LegalCopyright", "© 2011-2013 Pete Batard (GPL v3)" |             VALUE "LegalCopyright", "© 2011-2013 Pete Batard (GPL v3)" | ||||||
|             VALUE "LegalTrademarks", "http://www.gnu.org/copyleft/gpl.html" |             VALUE "LegalTrademarks", "http://www.gnu.org/copyleft/gpl.html" | ||||||
|             VALUE "OriginalFilename", "rufus.exe" |             VALUE "OriginalFilename", "rufus.exe" | ||||||
|             VALUE "ProductName", "Rufus" |             VALUE "ProductName", "Rufus" | ||||||
|             VALUE "ProductVersion", "1.3.4.253" |             VALUE "ProductVersion", "1.3.4.254" | ||||||
|         END |         END | ||||||
|     END |     END | ||||||
|     BLOCK "VarFileInfo" |     BLOCK "VarFileInfo" | ||||||
|  |  | ||||||
|  | @ -73,6 +73,7 @@ BOOL InstallSyslinux(DWORD drive_index, char drive_letter) | ||||||
| 	DWORD bytes_read; | 	DWORD bytes_read; | ||||||
| 	DWORD bytes_written; | 	DWORD bytes_written; | ||||||
| 	BOOL r = FALSE; | 	BOOL r = FALSE; | ||||||
|  | 	FILE* fd; | ||||||
| 
 | 
 | ||||||
| 	static unsigned char sectbuf[SECTOR_SIZE]; | 	static unsigned char sectbuf[SECTOR_SIZE]; | ||||||
| 	static LPSTR resource[2][2] = { | 	static LPSTR resource[2][2] = { | ||||||
|  | @ -80,6 +81,7 @@ BOOL InstallSyslinux(DWORD drive_index, char drive_letter) | ||||||
| 		{ MAKEINTRESOURCEA(IDR_SL_LDLINUX_V5_SYS),  MAKEINTRESOURCEA(IDR_SL_LDLINUX_V5_BSS) } }; | 		{ MAKEINTRESOURCEA(IDR_SL_LDLINUX_V5_SYS),  MAKEINTRESOURCEA(IDR_SL_LDLINUX_V5_BSS) } }; | ||||||
| 	static char ldlinux_path[] = "?:\\ldlinux.sys"; | 	static char ldlinux_path[] = "?:\\ldlinux.sys"; | ||||||
| 	static char* ldlinux_sys = &ldlinux_path[3]; | 	static char* ldlinux_sys = &ldlinux_path[3]; | ||||||
|  | 	const char* ldlinux_c32 = "ldlinux.c32"; | ||||||
| 	struct libfat_filesystem *fs; | 	struct libfat_filesystem *fs; | ||||||
| 	libfat_sector_t s, *secp; | 	libfat_sector_t s, *secp; | ||||||
| 	libfat_sector_t *sectors = NULL; | 	libfat_sector_t *sectors = NULL; | ||||||
|  | @ -202,6 +204,22 @@ BOOL InstallSyslinux(DWORD drive_index, char drive_letter) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	uprintf("Succesfully wrote Syslinux boot record\n"); | 	uprintf("Succesfully wrote Syslinux boot record\n"); | ||||||
|  | 
 | ||||||
|  | 	if (dt == DT_SYSLINUX_V5) { | ||||||
|  | 		fd = fopen(ldlinux_c32, "rb"); | ||||||
|  | 		if (fd == NULL) { | ||||||
|  | 			uprintf("Caution: No '%s' was provided. The target will be missing a mandatory Syslinux file!\n", ldlinux_c32); | ||||||
|  | 		} else { | ||||||
|  | 			fclose(fd); | ||||||
|  | 			ldlinux_path[11] = 'c'; ldlinux_path[12] = '3'; ldlinux_path[13] = '2'; | ||||||
|  | 			if (CopyFileA(ldlinux_c32, ldlinux_path, TRUE)) { | ||||||
|  | 				uprintf("Created '%s' (from local copy)", ldlinux_path); | ||||||
|  | 			} else { | ||||||
|  | 				uprintf("Failed to create '%s': %s\n", ldlinux_path, WindowsErrorString()); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	if (dt != DT_ISO) | 	if (dt != DT_ISO) | ||||||
| 		UpdateProgress(OP_DOS, -1.0f); | 		UpdateProgress(OP_DOS, -1.0f); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue