可喜可贺,在昏天黑地的做了一周界面后,我终于休假了!
最近着重做了密码认证部分,这里主要用到的控件是PASSWORD,这个控件跟代码交互的地方非常多,不是简单的把VFR写上去就行。所以这一篇也会以此控件为例,讲一下如何根据需要写HII驱动代码!
首先还来看这张图啦,左边的Text Display Engine与CustomizedDisplayLib是制作控件的两个模块。
顾名思义,Text Display Engine更注重控件构成的流程,CustomizedDisplayLib是控件组成元素的具体实现,听上去很绕对吧?以话剧为例,前者就像剧本,记录了什么时候该干什么,而后者就像是演员和道具,构成了剧本中需要的部分——当然,演员是可以换的,所以它是以LIB的形式存在,不同架构可以给出不同实现,我们可以换一个更大的对话框、或者给Setup界面左右各画一条竖线,但Text Display Engine我不建议改动,影响可能会比较大。
所以,当你要使用一个不熟的控件时,建议先去Text Display Engine中看一下它是如何实现的,比如PASSWORD。在控件实现的ProcessOptions函数中,调用了PasswordProcess去实现PASSWORD这个控件。
这里有个小技巧,就是Text Display Engine中控件的定义类型格式为EFI_IFR_控件名_OP,如果想要找具体控件的实现信息,可以直接搜。(我就是这么搜的233)
那么PasswordProcess做了什么呢?
//
// Use a NULL password to test whether old password is required
//
*StringPtr = 0;
Status = Question->PasswordCheck (gFormData, Question, StringPtr);
if ((Status == EFI_NOT_AVAILABLE_YET) || (Status == EFI_UNSUPPORTED)) {
//
// Password can't be set now.
//
if (Status == EFI_UNSUPPORTED) {
do {
CreateDialog (&Key, gEmptyString, gPasswordUnsupported, gPressEnter, gEmptyString, NULL);
} while (Key.UnicodeChar != CHAR_CARRIAGE_RETURN);
}
FreePool (StringPtr);
return EFI_SUCCESS;
}
根据注释,这一段是它传了一个NULL进PasswordCheck这个函数,看看是否存有旧密码,如果你有见过这个控件的应用就应该知道,在修改密码时,需要先输入正确的旧密码才能走后面的流程,对应的就是这里。
PasswordCheck的核心代码如下
if (PasswordString != NULL) {
IfrTypeValue.string = NewString (PasswordString, gCurrentSelection->FormSet->HiiHandle);
} else {
IfrTypeValue.string = 0;
}
//
// Send password to Configuration Driver for validation
//
Status = ConfigAccess->Callback (
ConfigAccess,
EFI_BROWSER_ACTION_CHANGING,
Question->QuestionId,
Question->HiiValue.Type,
&IfrTypeValue,
&ActionRequest
);
//
// Remove password string from HII database
//
if (PasswordString != NULL) {
DeleteString (IfrTypeValue.string, gCurrentSelection->FormSet->HiiHandle);
}
如果传进来的不是NULL(注意,这里传进来的是“”,不是NULL!),注册IfrTypeValue.string,然后调回界面的CallBack函数,这个CallBack函数做过界面的肯定不陌生,就是这个。
BOOT_MANAGER_CALLBACK_DATA gBootManagerPrivate = {
BOOT_MANAGER_CALLBACK_DATA_SIGNATURE,
NULL,
NULL,
{
BootManagerExtractConfig,
BootManagerRouteConfig,
BootManagerCallback
}
};
在UI链接库的入口函数里注册的HII 驱动,也就是我一开始说的,控件和界面交互的地方。
那么将这个函数的声明与CheckPasswd传入的参数结合起来看
EFI_STATUS
EFIAPI
BootManagerCallback (
IN CONST EFI_HII_CONFIG_ACCESS_PROTOCOL *This,
IN EFI_BROWSER_ACTION Action,
IN EFI_QUESTION_ID QuestionId,
IN UINT8 Type,
IN EFI_IFR_TYPE_VALUE *Value,
OUT EFI_BROWSER_ACTION_REQUEST *ActionRequest
)
{
Status = ConfigAccess->Callback (
ConfigAccess,
EFI_BROWSER_ACTION_CHANGING,
Question->QuestionId,
Question->HiiValue.Type,
&IfrTypeValue,
&ActionRequest
);
可以看到,我们传入了EFI_BROWSER_ACTION_CHANGING作为行为类型,有人问我为什么用的CHANGING而不是CHANGED,我第一反应是这人家官方代码写死的我能怎么办(,严肃的说,可能是因为此控件处于并没有完成功能(设置密码)状态,当然,是我的猜测。
同时,""作为IfrTypeValue.string被传回去了,同时还有控件的KEY值QuestionId,通过这个值可以在HII驱动里确定是哪个控件调回来的,这是上一章的内容。
那么在代码里,这一段的写法就很明确了。
if (action == CHANGING)
if (QuestionId == 0x0525) //0525是我在vfr中设置的此控件key值
if (Value->string = "")
return 是否有旧密码?
此处结束,我们回到PasswordProcess这个函数
Status = Question->PasswordCheck (gFormData, Question, StringPtr);
if ((Status == EFI_NOT_AVAILABLE_YET) || (Status == EFI_UNSUPPORTED)) {
//
// Password can't be set now.
//
if (Status == EFI_UNSUPPORTED) {
do {
CreateDialog (&Key, gEmptyString, gPasswordUnsupported, gPressEnter, gEmptyString, NULL);
} while (Key.UnicodeChar != CHAR_CARRIAGE_RETURN);
}
FreePool (StringPtr);
return EFI_SUCCESS;
}
如果是这两个错,认为此控件不支持,直接退出(呵呵,不知道是哪个小傻子把返回值设成了EFI_UNSUPPORTED,查了一天),其中,CreateDialog就是由CustomizedDisplayLib实现的。
如果不是这两个错,但不是成功,就说明有旧密码,进入以下流程
if (EFI_ERROR (Status)) {
//
// Old password exist, ask user for the old password
// 这个ReadString是封装好的一个弹窗,可以获取用户的输入,具体实现可以看代码,
//
Status = ReadString (MenuOption, gPromptForPassword, StringPtr);
if (EFI_ERROR (Status)) {
ZeroMem (StringPtr, (Maximum + 1) * sizeof (CHAR16));
FreePool (StringPtr);
return Status;
}
//
// Check user input old password
//
Status = Question->PasswordCheck (gFormData, Question, StringPtr);
if (EFI_ERROR (Status)) {
if (Status == EFI_NOT_READY) {
//
// Typed in old password incorrect
//
PasswordInvalid ();
} else {
Status = EFI_SUCCESS;
}
ZeroMem (StringPtr, (Maximum + 1) * sizeof (CHAR16));
FreePool (StringPtr);
}
将用户输入的旧密码再次传入PasswordCheck,与刚才类似,不过IfrTypeValue.string的内容不是“”,而是用户输入的字符串。
对应的,在回调函数中,我们就需要以下部分
if (action == CHANGING)
if (QuestionId == 0x0525) //0525是我在vfr中设置的此控件key值
if (Value->string != "")
return 旧密码是否正确?
接着往下看,如果验证正确,这个时候该进入正常的设置密码流程了
//
// Ask for new password
//
ZeroMem (StringPtr, (Maximum + 1) * sizeof (CHAR16));
Status = ReadString (MenuOption, gPromptForNewPassword, StringPtr);
if (EFI_ERROR (Status)) {
//
// Reset state machine for password
//
Question->PasswordCheck (gFormData, Question, NULL);
ZeroMem (StringPtr, (Maximum + 1) * sizeof (CHAR16));
FreePool (StringPtr);
return Status;
}
这里可以说一下,ReadString里封装了对于此控件的一些输入检测,比如输入过长、过短、两次输入不一致,如果没有通过检测,很不幸,它又调回PasswordCheck了,内容是NULL,对应的,传入回调函数的参数为NULL,所以回调函数里需要这一段。
if (action == CHANGING)
if (QuestionId == 0x0525) //0525是我在vfr中设置的此控件key值
if (Value->string == NULL)
做点输入错误内容的事情
return SUCCESS;
经过错误检测后,它认可了用户传入的新密码
然后这个函数就结束了
当时我一脸懵逼,啊?没了?那新密码是怎么传回去的?
于是我使用了搜索EFI_IFR_PASSWORD_OP大法,找到了这段
case EFI_IFR_PASSWORD_OP:
if (UserInput->InputValue.Buffer == NULL) {
//
// User not input new password, just return.
//
break;
}
DeleteString (Statement->HiiValue.Value.string, gCurrentSelection->FormSet->HiiHandle);
Statement->HiiValue.Value.string = UserInput->InputValue.Value.string;
CopyMem (Statement->BufferValue, UserInput->InputValue.Buffer, (UINTN)UserInput->InputValue.BufferLen);
ZeroMem (UserInput->InputValue.Buffer, (UINTN)UserInput->InputValue.BufferLen);
FreePool (UserInput->InputValue.Buffer);
//
// Two password match, send it to Configuration Driver
//
if ((Statement->QuestionFlags & EFI_IFR_FLAG_CALLBACK) != 0) {
PasswordCheck (NULL, UserInput->SelectedStatement, (CHAR16 *)Statement->BufferValue);
嗯,看着就像
浅提一下,这段出现在ProcessUserInput,用于处理用户输入,可以看到,它又调回了PasswordCheck,走的是与验证密码类似的流程
所以,回调函数里需要这一段
if (action == CHANGING)
if (QuestionId == 0x0525) //0525是我在vfr中设置的此控件key值
if (Value->string != "")
设置密码
return SUCCESS;
是的,你没看错,这判断方式和验证密码一样——也就是说,你要自己判断现在处于哪个阶段,是验证旧密码还是设置新密码?
类似的,取消密码用的是输入两次“”,这和控件传入“”判断是否有旧密码是同样的判断条件,也要判断。也意味着你需要测试各种情况,真正的写代码一天,改代码三天(手动微笑)。
不过这也算是相当复杂的控件了,其他控件会好一些。
界面还是很有趣的,我曾经抱怨EDK2的帮助文档说的语焉不详,一点不像QT的保姆教程,恨不得详细到小学生都能上手做,现在想想这也是这个框架有魅力的地方,根据底层构造去写代码、实现功能,也算是弥补了我一些没有选择QT为职业的遗憾吧。
哦对了,HII驱动其实除了CallBack函数,还有两个大哥,负责给控件写初始值和刷新值的,PASSWORD用不到,以后有机会细说吧(挖坑!)。